mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 23:01:48 +00:00
Format the shared package and drop an unused import to satisfy the lint gate
The i18n and schema changes added code that wasn't prettier-formatted, and place.schema.ts imported categorySchema without using it. Run prettier over shared and remove the import so 'npm run lint' + 'format:check' pass.
This commit is contained in:
@@ -1,33 +1,67 @@
|
|||||||
|
import {
|
||||||
|
adminUserCreateRequestSchema,
|
||||||
|
adminPermissionsRequestSchema,
|
||||||
|
adminInviteCreateRequestSchema,
|
||||||
|
adminFeatureToggleRequestSchema,
|
||||||
|
} from './admin.schema';
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { adminUserCreateRequestSchema, adminPermissionsRequestSchema, adminInviteCreateRequestSchema, adminFeatureToggleRequestSchema } from './admin.schema';
|
|
||||||
|
|
||||||
describe('adminUserCreateRequestSchema', () => {
|
describe('adminUserCreateRequestSchema', () => {
|
||||||
it('requires an email; role limited to user/admin', () => {
|
it('requires an email; role limited to user/admin', () => {
|
||||||
expect(adminUserCreateRequestSchema.safeParse({ email: 'a@b.c', password: 'p', role: 'admin' }).success).toBe(true);
|
expect(
|
||||||
expect(adminUserCreateRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(true);
|
adminUserCreateRequestSchema.safeParse({
|
||||||
expect(adminUserCreateRequestSchema.safeParse({ password: 'p' }).success).toBe(false);
|
email: 'a@b.c',
|
||||||
expect(adminUserCreateRequestSchema.safeParse({ email: 'a@b.c', role: 'root' }).success).toBe(false);
|
password: 'p',
|
||||||
|
role: 'admin',
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
adminUserCreateRequestSchema.safeParse({ email: 'a@b.c' }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
adminUserCreateRequestSchema.safeParse({ password: 'p' }).success,
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
adminUserCreateRequestSchema.safeParse({ email: 'a@b.c', role: 'root' })
|
||||||
|
.success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('adminPermissionsRequestSchema', () => {
|
describe('adminPermissionsRequestSchema', () => {
|
||||||
it('requires a permissions record', () => {
|
it('requires a permissions record', () => {
|
||||||
expect(adminPermissionsRequestSchema.safeParse({ permissions: { trip_edit: { user: true } } }).success).toBe(true);
|
expect(
|
||||||
|
adminPermissionsRequestSchema.safeParse({
|
||||||
|
permissions: { trip_edit: { user: true } },
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
expect(adminPermissionsRequestSchema.safeParse({}).success).toBe(false);
|
expect(adminPermissionsRequestSchema.safeParse({}).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('adminInviteCreateRequestSchema', () => {
|
describe('adminInviteCreateRequestSchema', () => {
|
||||||
it('accepts optional uses/expiry/role', () => {
|
it('accepts optional uses/expiry/role', () => {
|
||||||
expect(adminInviteCreateRequestSchema.safeParse({ max_uses: 5, expires_in_days: 7 }).success).toBe(true);
|
expect(
|
||||||
|
adminInviteCreateRequestSchema.safeParse({
|
||||||
|
max_uses: 5,
|
||||||
|
expires_in_days: 7,
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
expect(adminInviteCreateRequestSchema.safeParse({}).success).toBe(true);
|
expect(adminInviteCreateRequestSchema.safeParse({}).success).toBe(true);
|
||||||
expect(adminInviteCreateRequestSchema.safeParse({ role: 'root' }).success).toBe(false);
|
expect(
|
||||||
|
adminInviteCreateRequestSchema.safeParse({ role: 'root' }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('adminFeatureToggleRequestSchema', () => {
|
describe('adminFeatureToggleRequestSchema', () => {
|
||||||
it('requires a boolean enabled', () => {
|
it('requires a boolean enabled', () => {
|
||||||
expect(adminFeatureToggleRequestSchema.safeParse({ enabled: true }).success).toBe(true);
|
expect(
|
||||||
expect(adminFeatureToggleRequestSchema.safeParse({ enabled: 'yes' }).success).toBe(false);
|
adminFeatureToggleRequestSchema.safeParse({ enabled: true }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
adminFeatureToggleRequestSchema.safeParse({ enabled: 'yes' }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,21 +14,29 @@ export const adminUserCreateRequestSchema = z.object({
|
|||||||
username: z.string().optional(),
|
username: z.string().optional(),
|
||||||
role: z.enum(['user', 'admin']).optional(),
|
role: z.enum(['user', 'admin']).optional(),
|
||||||
});
|
});
|
||||||
export type AdminUserCreateRequest = z.infer<typeof adminUserCreateRequestSchema>;
|
export type AdminUserCreateRequest = z.infer<
|
||||||
|
typeof adminUserCreateRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const adminPermissionsRequestSchema = z.object({
|
export const adminPermissionsRequestSchema = z.object({
|
||||||
permissions: z.record(z.string(), z.unknown()),
|
permissions: z.record(z.string(), z.unknown()),
|
||||||
});
|
});
|
||||||
export type AdminPermissionsRequest = z.infer<typeof adminPermissionsRequestSchema>;
|
export type AdminPermissionsRequest = z.infer<
|
||||||
|
typeof adminPermissionsRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const adminInviteCreateRequestSchema = z.object({
|
export const adminInviteCreateRequestSchema = z.object({
|
||||||
max_uses: z.number().optional(),
|
max_uses: z.number().optional(),
|
||||||
expires_in_days: z.number().optional(),
|
expires_in_days: z.number().optional(),
|
||||||
role: z.enum(['user', 'admin']).optional(),
|
role: z.enum(['user', 'admin']).optional(),
|
||||||
});
|
});
|
||||||
export type AdminInviteCreateRequest = z.infer<typeof adminInviteCreateRequestSchema>;
|
export type AdminInviteCreateRequest = z.infer<
|
||||||
|
typeof adminInviteCreateRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const adminFeatureToggleRequestSchema = z.object({
|
export const adminFeatureToggleRequestSchema = z.object({
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
});
|
});
|
||||||
export type AdminFeatureToggleRequest = z.infer<typeof adminFeatureToggleRequestSchema>;
|
export type AdminFeatureToggleRequest = z.infer<
|
||||||
|
typeof adminFeatureToggleRequestSchema
|
||||||
|
>;
|
||||||
|
|||||||
@@ -1,20 +1,35 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { airportSchema, airportSearchQuerySchema } from './airport.schema';
|
import { airportSchema, airportSearchQuerySchema } from './airport.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('airportSchema', () => {
|
describe('airportSchema', () => {
|
||||||
it('accepts a full airport record', () => {
|
it('accepts a full airport record', () => {
|
||||||
const parsed = airportSchema.parse({
|
const parsed = airportSchema.parse({
|
||||||
iata: 'BER', icao: 'EDDB', name: 'Berlin Brandenburg', city: 'Berlin',
|
iata: 'BER',
|
||||||
country: 'DE', lat: 52.36, lng: 13.5, tz: 'Europe/Berlin',
|
icao: 'EDDB',
|
||||||
|
name: 'Berlin Brandenburg',
|
||||||
|
city: 'Berlin',
|
||||||
|
country: 'DE',
|
||||||
|
lat: 52.36,
|
||||||
|
lng: 13.5,
|
||||||
|
tz: 'Europe/Berlin',
|
||||||
});
|
});
|
||||||
expect(parsed.iata).toBe('BER');
|
expect(parsed.iata).toBe('BER');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows a null icao (smaller fields can be missing one)', () => {
|
it('allows a null icao (smaller fields can be missing one)', () => {
|
||||||
expect(airportSchema.safeParse({
|
expect(
|
||||||
iata: 'XXX', icao: null, name: 'Test', city: 'Test', country: 'DE',
|
airportSchema.safeParse({
|
||||||
lat: 0, lng: 0, tz: 'UTC',
|
iata: 'XXX',
|
||||||
}).success).toBe(true);
|
icao: null,
|
||||||
|
name: 'Test',
|
||||||
|
city: 'Test',
|
||||||
|
country: 'DE',
|
||||||
|
lat: 0,
|
||||||
|
lng: 0,
|
||||||
|
tz: 'UTC',
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,45 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
import {
|
||||||
assignmentCreateRequestSchema,
|
assignmentCreateRequestSchema,
|
||||||
assignmentMoveRequestSchema,
|
assignmentMoveRequestSchema,
|
||||||
assignmentParticipantsRequestSchema,
|
assignmentParticipantsRequestSchema,
|
||||||
} from './assignment.schema';
|
} from './assignment.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('assignmentCreateRequestSchema', () => {
|
describe('assignmentCreateRequestSchema', () => {
|
||||||
it('requires a place_id; notes optional/nullable', () => {
|
it('requires a place_id; notes optional/nullable', () => {
|
||||||
expect(assignmentCreateRequestSchema.safeParse({ place_id: 2 }).success).toBe(true);
|
expect(
|
||||||
expect(assignmentCreateRequestSchema.safeParse({ place_id: '2', notes: null }).success).toBe(true);
|
assignmentCreateRequestSchema.safeParse({ place_id: 2 }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
assignmentCreateRequestSchema.safeParse({ place_id: '2', notes: null })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
expect(assignmentCreateRequestSchema.safeParse({}).success).toBe(false);
|
expect(assignmentCreateRequestSchema.safeParse({}).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('assignmentMoveRequestSchema', () => {
|
describe('assignmentMoveRequestSchema', () => {
|
||||||
it('requires new_day_id; order_index optional', () => {
|
it('requires new_day_id; order_index optional', () => {
|
||||||
expect(assignmentMoveRequestSchema.safeParse({ new_day_id: 4 }).success).toBe(true);
|
expect(
|
||||||
expect(assignmentMoveRequestSchema.safeParse({ new_day_id: 4, order_index: 0 }).success).toBe(true);
|
assignmentMoveRequestSchema.safeParse({ new_day_id: 4 }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
assignmentMoveRequestSchema.safeParse({ new_day_id: 4, order_index: 0 })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
expect(assignmentMoveRequestSchema.safeParse({}).success).toBe(false);
|
expect(assignmentMoveRequestSchema.safeParse({}).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('assignmentParticipantsRequestSchema', () => {
|
describe('assignmentParticipantsRequestSchema', () => {
|
||||||
it('requires a numeric user_ids array', () => {
|
it('requires a numeric user_ids array', () => {
|
||||||
expect(assignmentParticipantsRequestSchema.safeParse({ user_ids: [1, 2] }).success).toBe(true);
|
expect(
|
||||||
expect(assignmentParticipantsRequestSchema.safeParse({ user_ids: 'no' }).success).toBe(false);
|
assignmentParticipantsRequestSchema.safeParse({ user_ids: [1, 2] })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
assignmentParticipantsRequestSchema.safeParse({ user_ids: 'no' }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
import { assignmentPlaceSchema } from '../place/place.schema';
|
import { assignmentPlaceSchema } from '../place/place.schema';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assignment API contract — single source of truth for the place↔day itinerary
|
* Assignment API contract — single source of truth for the place↔day itinerary
|
||||||
* endpoints under /api/trips/:tripId/days/:dayId/assignments and
|
* endpoints under /api/trips/:tripId/days/:dayId/assignments and
|
||||||
@@ -48,12 +49,16 @@ export const assignmentCreateRequestSchema = z.object({
|
|||||||
place_id: z.union([z.number(), z.string()]),
|
place_id: z.union([z.number(), z.string()]),
|
||||||
notes: z.string().nullable().optional(),
|
notes: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
export type AssignmentCreateRequest = z.infer<typeof assignmentCreateRequestSchema>;
|
export type AssignmentCreateRequest = z.infer<
|
||||||
|
typeof assignmentCreateRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const assignmentReorderRequestSchema = z.object({
|
export const assignmentReorderRequestSchema = z.object({
|
||||||
orderedIds: z.array(z.number()),
|
orderedIds: z.array(z.number()),
|
||||||
});
|
});
|
||||||
export type AssignmentReorderRequest = z.infer<typeof assignmentReorderRequestSchema>;
|
export type AssignmentReorderRequest = z.infer<
|
||||||
|
typeof assignmentReorderRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const assignmentMoveRequestSchema = z.object({
|
export const assignmentMoveRequestSchema = z.object({
|
||||||
new_day_id: z.union([z.number(), z.string()]),
|
new_day_id: z.union([z.number(), z.string()]),
|
||||||
@@ -70,4 +75,6 @@ export type AssignmentTimeRequest = z.infer<typeof assignmentTimeRequestSchema>;
|
|||||||
export const assignmentParticipantsRequestSchema = z.object({
|
export const assignmentParticipantsRequestSchema = z.object({
|
||||||
user_ids: z.array(z.number()),
|
user_ids: z.array(z.number()),
|
||||||
});
|
});
|
||||||
export type AssignmentParticipantsRequest = z.infer<typeof assignmentParticipantsRequestSchema>;
|
export type AssignmentParticipantsRequest = z.infer<
|
||||||
|
typeof assignmentParticipantsRequestSchema
|
||||||
|
>;
|
||||||
|
|||||||
@@ -1,29 +1,54 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
import {
|
||||||
markRegionRequestSchema,
|
markRegionRequestSchema,
|
||||||
createBucketItemRequestSchema,
|
createBucketItemRequestSchema,
|
||||||
regionGeoSchema,
|
regionGeoSchema,
|
||||||
} from './atlas.schema';
|
} from './atlas.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('markRegionRequestSchema', () => {
|
describe('markRegionRequestSchema', () => {
|
||||||
it('requires both name and country_code', () => {
|
it('requires both name and country_code', () => {
|
||||||
expect(markRegionRequestSchema.safeParse({ name: 'Bavaria', country_code: 'DE' }).success).toBe(true);
|
expect(
|
||||||
expect(markRegionRequestSchema.safeParse({ name: 'Bavaria' }).success).toBe(false);
|
markRegionRequestSchema.safeParse({ name: 'Bavaria', country_code: 'DE' })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(markRegionRequestSchema.safeParse({ name: 'Bavaria' }).success).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createBucketItemRequestSchema', () => {
|
describe('createBucketItemRequestSchema', () => {
|
||||||
it('requires a name; coordinates and metadata optional/nullable', () => {
|
it('requires a name; coordinates and metadata optional/nullable', () => {
|
||||||
expect(createBucketItemRequestSchema.safeParse({ name: 'Tokyo' }).success).toBe(true);
|
expect(
|
||||||
expect(createBucketItemRequestSchema.safeParse({ name: 'Tokyo', lat: 35, lng: 139, country_code: null }).success).toBe(true);
|
createBucketItemRequestSchema.safeParse({ name: 'Tokyo' }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
createBucketItemRequestSchema.safeParse({
|
||||||
|
name: 'Tokyo',
|
||||||
|
lat: 35,
|
||||||
|
lng: 139,
|
||||||
|
country_code: null,
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
expect(createBucketItemRequestSchema.safeParse({}).success).toBe(false);
|
expect(createBucketItemRequestSchema.safeParse({}).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('regionGeoSchema', () => {
|
describe('regionGeoSchema', () => {
|
||||||
it('accepts a FeatureCollection with opaque features', () => {
|
it('accepts a FeatureCollection with opaque features', () => {
|
||||||
expect(regionGeoSchema.safeParse({ type: 'FeatureCollection', features: [] }).success).toBe(true);
|
expect(
|
||||||
expect(regionGeoSchema.safeParse({ type: 'FeatureCollection', features: [{ anything: true }] }).success).toBe(true);
|
regionGeoSchema.safeParse({ type: 'FeatureCollection', features: [] })
|
||||||
expect(regionGeoSchema.safeParse({ type: 'Other', features: [] }).success).toBe(false);
|
.success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
regionGeoSchema.safeParse({
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: [{ anything: true }],
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
regionGeoSchema.safeParse({ type: 'Other', features: [] }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ export const createBucketItemRequestSchema = z.object({
|
|||||||
notes: z.string().nullable().optional(),
|
notes: z.string().nullable().optional(),
|
||||||
target_date: z.string().nullable().optional(),
|
target_date: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
export type CreateBucketItemRequest = z.infer<typeof createBucketItemRequestSchema>;
|
export type CreateBucketItemRequest = z.infer<
|
||||||
|
typeof createBucketItemRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const updateBucketItemRequestSchema = z.object({
|
export const updateBucketItemRequestSchema = z.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
@@ -39,7 +41,9 @@ export const updateBucketItemRequestSchema = z.object({
|
|||||||
country_code: z.string().nullable().optional(),
|
country_code: z.string().nullable().optional(),
|
||||||
target_date: z.string().nullable().optional(),
|
target_date: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
export type UpdateBucketItemRequest = z.infer<typeof updateBucketItemRequestSchema>;
|
export type UpdateBucketItemRequest = z.infer<
|
||||||
|
typeof updateBucketItemRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
/** A bucket-list item row (DB-shaped; kept open). */
|
/** A bucket-list item row (DB-shaped; kept open). */
|
||||||
export const bucketItemSchema = open;
|
export const bucketItemSchema = open;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
import {
|
||||||
registerRequestSchema,
|
registerRequestSchema,
|
||||||
loginRequestSchema,
|
loginRequestSchema,
|
||||||
@@ -10,38 +9,84 @@ import {
|
|||||||
mcpTokenCreateRequestSchema,
|
mcpTokenCreateRequestSchema,
|
||||||
} from './auth.schema';
|
} from './auth.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('registerRequestSchema', () => {
|
describe('registerRequestSchema', () => {
|
||||||
it('requires email + password; username/invite optional', () => {
|
it('requires email + password; username/invite optional', () => {
|
||||||
expect(registerRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' }).success).toBe(true);
|
expect(
|
||||||
expect(registerRequestSchema.safeParse({ email: 'a@b.c', password: 'pw', invite_token: 't' }).success).toBe(true);
|
registerRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' })
|
||||||
expect(registerRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(false);
|
.success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
registerRequestSchema.safeParse({
|
||||||
|
email: 'a@b.c',
|
||||||
|
password: 'pw',
|
||||||
|
invite_token: 't',
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(registerRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('loginRequestSchema', () => {
|
describe('loginRequestSchema', () => {
|
||||||
it('requires email + password', () => {
|
it('requires email + password', () => {
|
||||||
expect(loginRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' }).success).toBe(true);
|
expect(
|
||||||
expect(loginRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(false);
|
loginRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(loginRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('forgot/reset/change password schemas', () => {
|
describe('forgot/reset/change password schemas', () => {
|
||||||
it('validate their required fields', () => {
|
it('validate their required fields', () => {
|
||||||
expect(forgotPasswordRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(true);
|
expect(
|
||||||
expect(resetPasswordRequestSchema.safeParse({ token: 't', new_password: 'pw' }).success).toBe(true);
|
forgotPasswordRequestSchema.safeParse({ email: 'a@b.c' }).success,
|
||||||
expect(resetPasswordRequestSchema.safeParse({ token: 't', new_password: 'pw', mfa_code: '123456' }).success).toBe(true);
|
).toBe(true);
|
||||||
expect(resetPasswordRequestSchema.safeParse({ new_password: 'pw' }).success).toBe(false);
|
expect(
|
||||||
expect(changePasswordRequestSchema.safeParse({ current_password: 'a', new_password: 'b' }).success).toBe(true);
|
resetPasswordRequestSchema.safeParse({ token: 't', new_password: 'pw' })
|
||||||
expect(changePasswordRequestSchema.safeParse({ new_password: 'b' }).success).toBe(false);
|
.success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
resetPasswordRequestSchema.safeParse({
|
||||||
|
token: 't',
|
||||||
|
new_password: 'pw',
|
||||||
|
mfa_code: '123456',
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
resetPasswordRequestSchema.safeParse({ new_password: 'pw' }).success,
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
changePasswordRequestSchema.safeParse({
|
||||||
|
current_password: 'a',
|
||||||
|
new_password: 'b',
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
changePasswordRequestSchema.safeParse({ new_password: 'b' }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mfa + mcp-token schemas', () => {
|
describe('mfa + mcp-token schemas', () => {
|
||||||
it('validate their fields', () => {
|
it('validate their fields', () => {
|
||||||
expect(mfaVerifyLoginRequestSchema.safeParse({ mfa_token: 't', code: '123456' }).success).toBe(true);
|
expect(
|
||||||
expect(mfaVerifyLoginRequestSchema.safeParse({ mfa_token: 't' }).success).toBe(false);
|
mfaVerifyLoginRequestSchema.safeParse({ mfa_token: 't', code: '123456' })
|
||||||
expect(mfaEnableRequestSchema.safeParse({ code: '123456' }).success).toBe(true);
|
.success,
|
||||||
expect(mcpTokenCreateRequestSchema.safeParse({ name: 'CLI' }).success).toBe(true);
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
mfaVerifyLoginRequestSchema.safeParse({ mfa_token: 't' }).success,
|
||||||
|
).toBe(false);
|
||||||
|
expect(mfaEnableRequestSchema.safeParse({ code: '123456' }).success).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(mcpTokenCreateRequestSchema.safeParse({ name: 'CLI' }).success).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
expect(mcpTokenCreateRequestSchema.safeParse({}).success).toBe(true);
|
expect(mcpTokenCreateRequestSchema.safeParse({}).success).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { autoBackupSettingsRequestSchema } from './backup.schema';
|
import { autoBackupSettingsRequestSchema } from './backup.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('autoBackupSettingsRequestSchema', () => {
|
describe('autoBackupSettingsRequestSchema', () => {
|
||||||
it('accepts the known toggles and stays permissive for extras', () => {
|
it('accepts the known toggles and stays permissive for extras', () => {
|
||||||
expect(autoBackupSettingsRequestSchema.safeParse({ enabled: true, interval: 'daily', keep_days: 7 }).success).toBe(true);
|
expect(
|
||||||
expect(autoBackupSettingsRequestSchema.safeParse({ enabled: false, foo: 'bar' }).success).toBe(true);
|
autoBackupSettingsRequestSchema.safeParse({
|
||||||
|
enabled: true,
|
||||||
|
interval: 'daily',
|
||||||
|
keep_days: 7,
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
autoBackupSettingsRequestSchema.safeParse({ enabled: false, foo: 'bar' })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
expect(autoBackupSettingsRequestSchema.safeParse({}).success).toBe(true);
|
expect(autoBackupSettingsRequestSchema.safeParse({}).success).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects a non-boolean enabled', () => {
|
it('rejects a non-boolean enabled', () => {
|
||||||
expect(autoBackupSettingsRequestSchema.safeParse({ enabled: 'yes' }).success).toBe(false);
|
expect(
|
||||||
|
autoBackupSettingsRequestSchema.safeParse({ enabled: 'yes' }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,4 +16,6 @@ export const autoBackupSettingsRequestSchema = z
|
|||||||
time: z.string().optional(),
|
time: z.string().optional(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
export type AutoBackupSettingsRequest = z.infer<typeof autoBackupSettingsRequestSchema>;
|
export type AutoBackupSettingsRequest = z.infer<
|
||||||
|
typeof autoBackupSettingsRequestSchema
|
||||||
|
>;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
import {
|
||||||
budgetCreateItemRequestSchema,
|
budgetCreateItemRequestSchema,
|
||||||
budgetUpdateMembersRequestSchema,
|
budgetUpdateMembersRequestSchema,
|
||||||
@@ -6,31 +5,54 @@ import {
|
|||||||
budgetReorderItemsRequestSchema,
|
budgetReorderItemsRequestSchema,
|
||||||
} from './budget.schema';
|
} from './budget.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('budgetCreateItemRequestSchema', () => {
|
describe('budgetCreateItemRequestSchema', () => {
|
||||||
it('requires a name; money/meta fields optional + nullable', () => {
|
it('requires a name; money/meta fields optional + nullable', () => {
|
||||||
expect(budgetCreateItemRequestSchema.safeParse({ name: 'Hotel' }).success).toBe(true);
|
expect(
|
||||||
expect(budgetCreateItemRequestSchema.safeParse({ name: 'Hotel', total_price: 200, persons: null }).success).toBe(true);
|
budgetCreateItemRequestSchema.safeParse({ name: 'Hotel' }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
budgetCreateItemRequestSchema.safeParse({
|
||||||
|
name: 'Hotel',
|
||||||
|
total_price: 200,
|
||||||
|
persons: null,
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
expect(budgetCreateItemRequestSchema.safeParse({}).success).toBe(false);
|
expect(budgetCreateItemRequestSchema.safeParse({}).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('budgetUpdateMembersRequestSchema', () => {
|
describe('budgetUpdateMembersRequestSchema', () => {
|
||||||
it('requires a numeric user_ids array', () => {
|
it('requires a numeric user_ids array', () => {
|
||||||
expect(budgetUpdateMembersRequestSchema.safeParse({ user_ids: [1, 2] }).success).toBe(true);
|
expect(
|
||||||
expect(budgetUpdateMembersRequestSchema.safeParse({ user_ids: 'no' }).success).toBe(false);
|
budgetUpdateMembersRequestSchema.safeParse({ user_ids: [1, 2] }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
budgetUpdateMembersRequestSchema.safeParse({ user_ids: 'no' }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('budgetToggleMemberPaidRequestSchema', () => {
|
describe('budgetToggleMemberPaidRequestSchema', () => {
|
||||||
it('requires a boolean paid', () => {
|
it('requires a boolean paid', () => {
|
||||||
expect(budgetToggleMemberPaidRequestSchema.safeParse({ paid: true }).success).toBe(true);
|
expect(
|
||||||
expect(budgetToggleMemberPaidRequestSchema.safeParse({ paid: 'yes' }).success).toBe(false);
|
budgetToggleMemberPaidRequestSchema.safeParse({ paid: true }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
budgetToggleMemberPaidRequestSchema.safeParse({ paid: 'yes' }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('budgetReorderItemsRequestSchema', () => {
|
describe('budgetReorderItemsRequestSchema', () => {
|
||||||
it('requires numeric ids', () => {
|
it('requires numeric ids', () => {
|
||||||
expect(budgetReorderItemsRequestSchema.safeParse({ orderedIds: [3, 1, 2] }).success).toBe(true);
|
expect(
|
||||||
expect(budgetReorderItemsRequestSchema.safeParse({ orderedIds: ['a'] }).success).toBe(false);
|
budgetReorderItemsRequestSchema.safeParse({ orderedIds: [3, 1, 2] })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
budgetReorderItemsRequestSchema.safeParse({ orderedIds: ['a'] }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -59,7 +59,9 @@ export const budgetCreateItemRequestSchema = z.object({
|
|||||||
note: z.string().nullable().optional(),
|
note: z.string().nullable().optional(),
|
||||||
expense_date: z.string().nullable().optional(),
|
expense_date: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
export type BudgetCreateItemRequest = z.infer<typeof budgetCreateItemRequestSchema>;
|
export type BudgetCreateItemRequest = z.infer<
|
||||||
|
typeof budgetCreateItemRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
/** Update accepts the same fields plus total_price changes; all optional. */
|
/** Update accepts the same fields plus total_price changes; all optional. */
|
||||||
export const budgetUpdateItemRequestSchema = z.object({
|
export const budgetUpdateItemRequestSchema = z.object({
|
||||||
@@ -71,24 +73,34 @@ export const budgetUpdateItemRequestSchema = z.object({
|
|||||||
note: z.string().nullable().optional(),
|
note: z.string().nullable().optional(),
|
||||||
expense_date: z.string().nullable().optional(),
|
expense_date: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
export type BudgetUpdateItemRequest = z.infer<typeof budgetUpdateItemRequestSchema>;
|
export type BudgetUpdateItemRequest = z.infer<
|
||||||
|
typeof budgetUpdateItemRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const budgetUpdateMembersRequestSchema = z.object({
|
export const budgetUpdateMembersRequestSchema = z.object({
|
||||||
user_ids: z.array(z.number()),
|
user_ids: z.array(z.number()),
|
||||||
});
|
});
|
||||||
export type BudgetUpdateMembersRequest = z.infer<typeof budgetUpdateMembersRequestSchema>;
|
export type BudgetUpdateMembersRequest = z.infer<
|
||||||
|
typeof budgetUpdateMembersRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const budgetToggleMemberPaidRequestSchema = z.object({
|
export const budgetToggleMemberPaidRequestSchema = z.object({
|
||||||
paid: z.boolean(),
|
paid: z.boolean(),
|
||||||
});
|
});
|
||||||
export type BudgetToggleMemberPaidRequest = z.infer<typeof budgetToggleMemberPaidRequestSchema>;
|
export type BudgetToggleMemberPaidRequest = z.infer<
|
||||||
|
typeof budgetToggleMemberPaidRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const budgetReorderItemsRequestSchema = z.object({
|
export const budgetReorderItemsRequestSchema = z.object({
|
||||||
orderedIds: z.array(z.number()),
|
orderedIds: z.array(z.number()),
|
||||||
});
|
});
|
||||||
export type BudgetReorderItemsRequest = z.infer<typeof budgetReorderItemsRequestSchema>;
|
export type BudgetReorderItemsRequest = z.infer<
|
||||||
|
typeof budgetReorderItemsRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const budgetReorderCategoriesRequestSchema = z.object({
|
export const budgetReorderCategoriesRequestSchema = z.object({
|
||||||
orderedCategories: z.array(z.string()),
|
orderedCategories: z.array(z.string()),
|
||||||
});
|
});
|
||||||
export type BudgetReorderCategoriesRequest = z.infer<typeof budgetReorderCategoriesRequestSchema>;
|
export type BudgetReorderCategoriesRequest = z.infer<
|
||||||
|
typeof budgetReorderCategoriesRequestSchema
|
||||||
|
>;
|
||||||
|
|||||||
@@ -1,20 +1,32 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
import {
|
||||||
categorySchema,
|
categorySchema,
|
||||||
createCategoryRequestSchema,
|
createCategoryRequestSchema,
|
||||||
updateCategoryRequestSchema,
|
updateCategoryRequestSchema,
|
||||||
} from './category.schema';
|
} from './category.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('categorySchema', () => {
|
describe('categorySchema', () => {
|
||||||
it('accepts a full category', () => {
|
it('accepts a full category', () => {
|
||||||
expect(categorySchema.safeParse({ id: 1, name: 'Food', color: '#fff', icon: '🍔' }).success).toBe(true);
|
expect(
|
||||||
|
categorySchema.safeParse({
|
||||||
|
id: 1,
|
||||||
|
name: 'Food',
|
||||||
|
color: '#fff',
|
||||||
|
icon: '🍔',
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createCategoryRequestSchema', () => {
|
describe('createCategoryRequestSchema', () => {
|
||||||
it('requires a non-empty name; colour and icon are optional', () => {
|
it('requires a non-empty name; colour and icon are optional', () => {
|
||||||
expect(createCategoryRequestSchema.safeParse({ name: 'Food' }).success).toBe(true);
|
expect(
|
||||||
expect(createCategoryRequestSchema.safeParse({ name: '' }).success).toBe(false);
|
createCategoryRequestSchema.safeParse({ name: 'Food' }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(createCategoryRequestSchema.safeParse({ name: '' }).success).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
expect(createCategoryRequestSchema.safeParse({}).success).toBe(false);
|
expect(createCategoryRequestSchema.safeParse({}).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -22,6 +34,8 @@ describe('createCategoryRequestSchema', () => {
|
|||||||
describe('updateCategoryRequestSchema', () => {
|
describe('updateCategoryRequestSchema', () => {
|
||||||
it('allows every field to be omitted (the service COALESCEs)', () => {
|
it('allows every field to be omitted (the service COALESCEs)', () => {
|
||||||
expect(updateCategoryRequestSchema.safeParse({}).success).toBe(true);
|
expect(updateCategoryRequestSchema.safeParse({}).success).toBe(true);
|
||||||
expect(updateCategoryRequestSchema.safeParse({ color: '#000' }).success).toBe(true);
|
expect(
|
||||||
|
updateCategoryRequestSchema.safeParse({ color: '#000' }).success,
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
import {
|
||||||
collabNoteCreateRequestSchema,
|
collabNoteCreateRequestSchema,
|
||||||
collabPollCreateRequestSchema,
|
collabPollCreateRequestSchema,
|
||||||
@@ -7,41 +6,78 @@ import {
|
|||||||
collabReactionRequestSchema,
|
collabReactionRequestSchema,
|
||||||
} from './collab.schema';
|
} from './collab.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('collabNoteCreateRequestSchema', () => {
|
describe('collabNoteCreateRequestSchema', () => {
|
||||||
it('requires a non-empty title; the rest is optional', () => {
|
it('requires a non-empty title; the rest is optional', () => {
|
||||||
expect(collabNoteCreateRequestSchema.safeParse({ title: 'Idea' }).success).toBe(true);
|
expect(
|
||||||
expect(collabNoteCreateRequestSchema.safeParse({ title: '' }).success).toBe(false);
|
collabNoteCreateRequestSchema.safeParse({ title: 'Idea' }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(collabNoteCreateRequestSchema.safeParse({ title: '' }).success).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
expect(collabNoteCreateRequestSchema.safeParse({}).success).toBe(false);
|
expect(collabNoteCreateRequestSchema.safeParse({}).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('collabPollCreateRequestSchema', () => {
|
describe('collabPollCreateRequestSchema', () => {
|
||||||
it('requires a question and at least two options', () => {
|
it('requires a question and at least two options', () => {
|
||||||
expect(collabPollCreateRequestSchema.safeParse({ question: 'Where?', options: ['A', 'B'] }).success).toBe(true);
|
expect(
|
||||||
expect(collabPollCreateRequestSchema.safeParse({ question: 'Where?', options: ['A'] }).success).toBe(false);
|
collabPollCreateRequestSchema.safeParse({
|
||||||
expect(collabPollCreateRequestSchema.safeParse({ options: ['A', 'B'] }).success).toBe(false);
|
question: 'Where?',
|
||||||
|
options: ['A', 'B'],
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
collabPollCreateRequestSchema.safeParse({
|
||||||
|
question: 'Where?',
|
||||||
|
options: ['A'],
|
||||||
|
}).success,
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
collabPollCreateRequestSchema.safeParse({ options: ['A', 'B'] }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('collabPollVoteRequestSchema', () => {
|
describe('collabPollVoteRequestSchema', () => {
|
||||||
it('requires a numeric option_index', () => {
|
it('requires a numeric option_index', () => {
|
||||||
expect(collabPollVoteRequestSchema.safeParse({ option_index: 0 }).success).toBe(true);
|
expect(
|
||||||
expect(collabPollVoteRequestSchema.safeParse({ option_index: 'a' }).success).toBe(false);
|
collabPollVoteRequestSchema.safeParse({ option_index: 0 }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
collabPollVoteRequestSchema.safeParse({ option_index: 'a' }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('collabMessageCreateRequestSchema', () => {
|
describe('collabMessageCreateRequestSchema', () => {
|
||||||
it('requires text, caps it at 5000, allows a nullable reply_to', () => {
|
it('requires text, caps it at 5000, allows a nullable reply_to', () => {
|
||||||
expect(collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: null }).success).toBe(true);
|
expect(
|
||||||
expect(collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: 4 }).success).toBe(true);
|
collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: null })
|
||||||
expect(collabMessageCreateRequestSchema.safeParse({ text: '' }).success).toBe(false);
|
.success,
|
||||||
expect(collabMessageCreateRequestSchema.safeParse({ text: 'x'.repeat(5001) }).success).toBe(false);
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: 4 })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
collabMessageCreateRequestSchema.safeParse({ text: '' }).success,
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
collabMessageCreateRequestSchema.safeParse({ text: 'x'.repeat(5001) })
|
||||||
|
.success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('collabReactionRequestSchema', () => {
|
describe('collabReactionRequestSchema', () => {
|
||||||
it('requires a non-empty emoji', () => {
|
it('requires a non-empty emoji', () => {
|
||||||
expect(collabReactionRequestSchema.safeParse({ emoji: '👍' }).success).toBe(true);
|
expect(collabReactionRequestSchema.safeParse({ emoji: '👍' }).success).toBe(
|
||||||
expect(collabReactionRequestSchema.safeParse({ emoji: '' }).success).toBe(false);
|
true,
|
||||||
|
);
|
||||||
|
expect(collabReactionRequestSchema.safeParse({ emoji: '' }).success).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ export const collabNoteCreateRequestSchema = z.object({
|
|||||||
color: z.string().optional(),
|
color: z.string().optional(),
|
||||||
website: z.string().optional(),
|
website: z.string().optional(),
|
||||||
});
|
});
|
||||||
export type CollabNoteCreateRequest = z.infer<typeof collabNoteCreateRequestSchema>;
|
export type CollabNoteCreateRequest = z.infer<
|
||||||
|
typeof collabNoteCreateRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const collabNoteUpdateRequestSchema = z.object({
|
export const collabNoteUpdateRequestSchema = z.object({
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
@@ -28,7 +30,9 @@ export const collabNoteUpdateRequestSchema = z.object({
|
|||||||
pinned: z.union([z.boolean(), z.number()]).optional(),
|
pinned: z.union([z.boolean(), z.number()]).optional(),
|
||||||
website: z.string().optional(),
|
website: z.string().optional(),
|
||||||
});
|
});
|
||||||
export type CollabNoteUpdateRequest = z.infer<typeof collabNoteUpdateRequestSchema>;
|
export type CollabNoteUpdateRequest = z.infer<
|
||||||
|
typeof collabNoteUpdateRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const collabPollCreateRequestSchema = z.object({
|
export const collabPollCreateRequestSchema = z.object({
|
||||||
question: z.string().min(1),
|
question: z.string().min(1),
|
||||||
@@ -37,7 +41,9 @@ export const collabPollCreateRequestSchema = z.object({
|
|||||||
multiple_choice: z.boolean().optional(),
|
multiple_choice: z.boolean().optional(),
|
||||||
deadline: z.string().optional(),
|
deadline: z.string().optional(),
|
||||||
});
|
});
|
||||||
export type CollabPollCreateRequest = z.infer<typeof collabPollCreateRequestSchema>;
|
export type CollabPollCreateRequest = z.infer<
|
||||||
|
typeof collabPollCreateRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const collabPollVoteRequestSchema = z.object({
|
export const collabPollVoteRequestSchema = z.object({
|
||||||
option_index: z.number(),
|
option_index: z.number(),
|
||||||
@@ -48,7 +54,9 @@ export const collabMessageCreateRequestSchema = z.object({
|
|||||||
text: z.string().min(1).max(5000),
|
text: z.string().min(1).max(5000),
|
||||||
reply_to: z.number().nullable().optional(),
|
reply_to: z.number().nullable().optional(),
|
||||||
});
|
});
|
||||||
export type CollabMessageCreateRequest = z.infer<typeof collabMessageCreateRequestSchema>;
|
export type CollabMessageCreateRequest = z.infer<
|
||||||
|
typeof collabMessageCreateRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const collabReactionRequestSchema = z.object({
|
export const collabReactionRequestSchema = z.object({
|
||||||
emoji: z.string().min(1),
|
emoji: z.string().min(1),
|
||||||
|
|||||||
@@ -1,30 +1,49 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
import {
|
||||||
dayCreateRequestSchema,
|
dayCreateRequestSchema,
|
||||||
dayNoteCreateRequestSchema,
|
dayNoteCreateRequestSchema,
|
||||||
dayNoteUpdateRequestSchema,
|
dayNoteUpdateRequestSchema,
|
||||||
} from './day.schema';
|
} from './day.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('dayCreateRequestSchema', () => {
|
describe('dayCreateRequestSchema', () => {
|
||||||
it('accepts an optional date + notes', () => {
|
it('accepts an optional date + notes', () => {
|
||||||
expect(dayCreateRequestSchema.safeParse({}).success).toBe(true);
|
expect(dayCreateRequestSchema.safeParse({}).success).toBe(true);
|
||||||
expect(dayCreateRequestSchema.safeParse({ date: '2026-07-01', notes: 'n' }).success).toBe(true);
|
expect(
|
||||||
|
dayCreateRequestSchema.safeParse({ date: '2026-07-01', notes: 'n' })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('dayNoteCreateRequestSchema', () => {
|
describe('dayNoteCreateRequestSchema', () => {
|
||||||
it('requires non-empty text capped at 500, time capped at 150', () => {
|
it('requires non-empty text capped at 500, time capped at 150', () => {
|
||||||
expect(dayNoteCreateRequestSchema.safeParse({ text: 'Lunch' }).success).toBe(true);
|
expect(
|
||||||
expect(dayNoteCreateRequestSchema.safeParse({ text: '' }).success).toBe(false);
|
dayNoteCreateRequestSchema.safeParse({ text: 'Lunch' }).success,
|
||||||
expect(dayNoteCreateRequestSchema.safeParse({ text: 'x'.repeat(501) }).success).toBe(false);
|
).toBe(true);
|
||||||
expect(dayNoteCreateRequestSchema.safeParse({ text: 'ok', time: 'y'.repeat(151) }).success).toBe(false);
|
expect(dayNoteCreateRequestSchema.safeParse({ text: '' }).success).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
dayNoteCreateRequestSchema.safeParse({ text: 'x'.repeat(501) }).success,
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
dayNoteCreateRequestSchema.safeParse({
|
||||||
|
text: 'ok',
|
||||||
|
time: 'y'.repeat(151),
|
||||||
|
}).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('dayNoteUpdateRequestSchema', () => {
|
describe('dayNoteUpdateRequestSchema', () => {
|
||||||
it('allows omitting text and caps the lengths', () => {
|
it('allows omitting text and caps the lengths', () => {
|
||||||
expect(dayNoteUpdateRequestSchema.safeParse({}).success).toBe(true);
|
expect(dayNoteUpdateRequestSchema.safeParse({}).success).toBe(true);
|
||||||
expect(dayNoteUpdateRequestSchema.safeParse({ icon: '🍽️' }).success).toBe(true);
|
expect(dayNoteUpdateRequestSchema.safeParse({ icon: '🍽️' }).success).toBe(
|
||||||
expect(dayNoteUpdateRequestSchema.safeParse({ text: 'x'.repeat(501) }).success).toBe(false);
|
true,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
dayNoteUpdateRequestSchema.safeParse({ text: 'x'.repeat(501) }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
import { assignmentSchema } from '../assignment/assignment.schema';
|
import { assignmentSchema } from '../assignment/assignment.schema';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Day + day-note API contract — single source of truth for the
|
* Day + day-note API contract — single source of truth for the
|
||||||
* /api/trips/:tripId/days and /api/trips/:tripId/days/:dayId/notes endpoints.
|
* /api/trips/:tripId/days and /api/trips/:tripId/days/:dayId/notes endpoints.
|
||||||
|
|||||||
@@ -1,18 +1,34 @@
|
|||||||
|
import {
|
||||||
|
fileUpdateRequestSchema,
|
||||||
|
fileLinkRequestSchema,
|
||||||
|
photoVariantSchema,
|
||||||
|
} from './file.schema';
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { fileUpdateRequestSchema, fileLinkRequestSchema, photoVariantSchema } from './file.schema';
|
|
||||||
|
|
||||||
describe('fileUpdateRequestSchema', () => {
|
describe('fileUpdateRequestSchema', () => {
|
||||||
it('accepts optional metadata, nullable ids, an empty body', () => {
|
it('accepts optional metadata, nullable ids, an empty body', () => {
|
||||||
expect(fileUpdateRequestSchema.safeParse({ description: 'doc', place_id: 3 }).success).toBe(true);
|
expect(
|
||||||
expect(fileUpdateRequestSchema.safeParse({ place_id: null, reservation_id: '7' }).success).toBe(true);
|
fileUpdateRequestSchema.safeParse({ description: 'doc', place_id: 3 })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
fileUpdateRequestSchema.safeParse({ place_id: null, reservation_id: '7' })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
expect(fileUpdateRequestSchema.safeParse({}).success).toBe(true);
|
expect(fileUpdateRequestSchema.safeParse({}).success).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fileLinkRequestSchema', () => {
|
describe('fileLinkRequestSchema', () => {
|
||||||
it('accepts any subset of reservation/assignment/place ids', () => {
|
it('accepts any subset of reservation/assignment/place ids', () => {
|
||||||
expect(fileLinkRequestSchema.safeParse({ reservation_id: 1 }).success).toBe(true);
|
expect(fileLinkRequestSchema.safeParse({ reservation_id: 1 }).success).toBe(
|
||||||
expect(fileLinkRequestSchema.safeParse({ assignment_id: '2', place_id: null }).success).toBe(true);
|
true,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
fileLinkRequestSchema.safeParse({ assignment_id: '2', place_id: null })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
expect(fileLinkRequestSchema.safeParse({}).success).toBe(true);
|
expect(fileLinkRequestSchema.safeParse({}).success).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -146,7 +146,8 @@ const dashboard: TranslationStrings = {
|
|||||||
'dashboard.confirm.copy.willCopy': 'Se copiará',
|
'dashboard.confirm.copy.willCopy': 'Se copiará',
|
||||||
'dashboard.confirm.copy.will1': 'Días, lugares y asignaciones por día',
|
'dashboard.confirm.copy.will1': 'Días, lugares y asignaciones por día',
|
||||||
'dashboard.confirm.copy.will2': 'Alojamientos y reservas',
|
'dashboard.confirm.copy.will2': 'Alojamientos y reservas',
|
||||||
'dashboard.confirm.copy.will3': 'Partidas de presupuesto y orden de categorías',
|
'dashboard.confirm.copy.will3':
|
||||||
|
'Partidas de presupuesto y orden de categorías',
|
||||||
'dashboard.confirm.copy.will4': 'Listas de equipaje (sin marcar)',
|
'dashboard.confirm.copy.will4': 'Listas de equipaje (sin marcar)',
|
||||||
'dashboard.confirm.copy.will5': 'Tareas (sin asignar ni marcar)',
|
'dashboard.confirm.copy.will5': 'Tareas (sin asignar ni marcar)',
|
||||||
'dashboard.confirm.copy.will6': 'Notas del día',
|
'dashboard.confirm.copy.will6': 'Notas del día',
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ const dashboard: TranslationStrings = {
|
|||||||
'dashboard.mobile.currencyConverter': 'Convertisseur de devises',
|
'dashboard.mobile.currencyConverter': 'Convertisseur de devises',
|
||||||
'dashboard.filter.planned': 'Planifiés',
|
'dashboard.filter.planned': 'Planifiés',
|
||||||
'dashboard.hero.badgeLive': 'EN DIRECT',
|
'dashboard.hero.badgeLive': 'EN DIRECT',
|
||||||
'dashboard.hero.badgeToday': 'DÉBUTE AUJOURD\'HUI',
|
'dashboard.hero.badgeToday': "DÉBUTE AUJOURD'HUI",
|
||||||
'dashboard.hero.badgeTomorrow': 'DEMAIN',
|
'dashboard.hero.badgeTomorrow': 'DEMAIN',
|
||||||
'dashboard.hero.badgeNext': 'À SUIVRE',
|
'dashboard.hero.badgeNext': 'À SUIVRE',
|
||||||
'dashboard.hero.badgeRecent': 'RÉCENT',
|
'dashboard.hero.badgeRecent': 'RÉCENT',
|
||||||
@@ -135,16 +135,17 @@ const dashboard: TranslationStrings = {
|
|||||||
'dashboard.atlas.acrossAllTrips': 'sur tous les voyages',
|
'dashboard.atlas.acrossAllTrips': 'sur tous les voyages',
|
||||||
'dashboard.atlas.distanceFlown': 'Distance parcourue en avion',
|
'dashboard.atlas.distanceFlown': 'Distance parcourue en avion',
|
||||||
'dashboard.atlas.kmUnit': 'km',
|
'dashboard.atlas.kmUnit': 'km',
|
||||||
'dashboard.atlas.aroundEquator': '≈ {count}× le tour de l\'équateur',
|
'dashboard.atlas.aroundEquator': "≈ {count}× le tour de l'équateur",
|
||||||
'dashboard.card.idea': 'Idée',
|
'dashboard.card.idea': 'Idée',
|
||||||
'dashboard.card.buddyOne': 'Compagnon',
|
'dashboard.card.buddyOne': 'Compagnon',
|
||||||
'dashboard.fx.from': 'De',
|
'dashboard.fx.from': 'De',
|
||||||
'dashboard.fx.to': 'Vers',
|
'dashboard.fx.to': 'Vers',
|
||||||
'dashboard.fx.unavailable': 'Taux indisponible',
|
'dashboard.fx.unavailable': 'Taux indisponible',
|
||||||
'dashboard.tz.searchPlaceholder': 'Rechercher un fuseau horaire…',
|
'dashboard.tz.searchPlaceholder': 'Rechercher un fuseau horaire…',
|
||||||
'dashboard.tz.empty': 'Pas encore d\'autres fuseaux horaires — ajoutez-en un avec +',
|
'dashboard.tz.empty':
|
||||||
|
"Pas encore d'autres fuseaux horaires — ajoutez-en un avec +",
|
||||||
'dashboard.upcoming.title': 'Prochaines réservations',
|
'dashboard.upcoming.title': 'Prochaines réservations',
|
||||||
'dashboard.upcoming.empty': 'Rien de réservé pour l\'instant.',
|
'dashboard.upcoming.empty': "Rien de réservé pour l'instant.",
|
||||||
'dashboard.confirm.copy.title': 'Copier ce voyage ?',
|
'dashboard.confirm.copy.title': 'Copier ce voyage ?',
|
||||||
'dashboard.confirm.copy.willCopy': 'Sera copié',
|
'dashboard.confirm.copy.willCopy': 'Sera copié',
|
||||||
'dashboard.confirm.copy.will1': 'Jours, lieux et affectations par jour',
|
'dashboard.confirm.copy.will1': 'Jours, lieux et affectations par jour',
|
||||||
@@ -159,7 +160,7 @@ const dashboard: TranslationStrings = {
|
|||||||
'dashboard.confirm.copy.wont3': 'Fichiers et photos',
|
'dashboard.confirm.copy.wont3': 'Fichiers et photos',
|
||||||
'dashboard.confirm.copy.wont4': 'Jetons de partage',
|
'dashboard.confirm.copy.wont4': 'Jetons de partage',
|
||||||
'dashboard.confirm.copy.confirm': 'Copier le voyage',
|
'dashboard.confirm.copy.confirm': 'Copier le voyage',
|
||||||
'dashboard.aria.toggleView': 'Changer d\'affichage',
|
'dashboard.aria.toggleView': "Changer d'affichage",
|
||||||
'dashboard.aria.filter': 'Filtrer',
|
'dashboard.aria.filter': 'Filtrer',
|
||||||
'dashboard.aria.duplicate': 'Dupliquer',
|
'dashboard.aria.duplicate': 'Dupliquer',
|
||||||
'dashboard.aria.refreshRates': 'Actualiser les taux',
|
'dashboard.aria.refreshRates': 'Actualiser les taux',
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
// @ts-expect-error — plain .mjs script with no .d.ts; import as JS module.
|
// @ts-expect-error — plain .mjs script with no .d.ts; import as JS module.
|
||||||
import { checkParity } from '../../scripts/i18n-parity.mjs';
|
import { checkParity } from '../../scripts/i18n-parity.mjs';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enforces the file-set contract for the i18n migration: every non-en locale
|
* Enforces the file-set contract for the i18n migration: every non-en locale
|
||||||
* dir must contain the exact same domain files as en/.
|
* dir must contain the exact same domain files as en/.
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ const dashboard: TranslationStrings = {
|
|||||||
'dashboard.hero.dayLeft': 'Giorno rimasto',
|
'dashboard.hero.dayLeft': 'Giorno rimasto',
|
||||||
'dashboard.hero.daysLeft': 'Giorni rimasti',
|
'dashboard.hero.daysLeft': 'Giorni rimasti',
|
||||||
'dashboard.hero.lastDay': 'Ultimo giorno',
|
'dashboard.hero.lastDay': 'Ultimo giorno',
|
||||||
'dashboard.hero.untilStart': 'All\'inizio',
|
'dashboard.hero.untilStart': "All'inizio",
|
||||||
'dashboard.hero.startsIn': 'Si parte tra',
|
'dashboard.hero.startsIn': 'Si parte tra',
|
||||||
'dashboard.atlas.countriesVisited': 'Atlas · Paesi visitati',
|
'dashboard.atlas.countriesVisited': 'Atlas · Paesi visitati',
|
||||||
'dashboard.atlas.ofTotal': 'di {total}',
|
'dashboard.atlas.ofTotal': 'di {total}',
|
||||||
@@ -135,14 +135,15 @@ const dashboard: TranslationStrings = {
|
|||||||
'dashboard.atlas.acrossAllTrips': 'su tutti i viaggi',
|
'dashboard.atlas.acrossAllTrips': 'su tutti i viaggi',
|
||||||
'dashboard.atlas.distanceFlown': 'Distanza in volo',
|
'dashboard.atlas.distanceFlown': 'Distanza in volo',
|
||||||
'dashboard.atlas.kmUnit': 'km',
|
'dashboard.atlas.kmUnit': 'km',
|
||||||
'dashboard.atlas.aroundEquator': '≈ {count}× intorno all\'equatore',
|
'dashboard.atlas.aroundEquator': "≈ {count}× intorno all'equatore",
|
||||||
'dashboard.card.idea': 'Idea',
|
'dashboard.card.idea': 'Idea',
|
||||||
'dashboard.card.buddyOne': 'Compagno',
|
'dashboard.card.buddyOne': 'Compagno',
|
||||||
'dashboard.fx.from': 'Da',
|
'dashboard.fx.from': 'Da',
|
||||||
'dashboard.fx.to': 'A',
|
'dashboard.fx.to': 'A',
|
||||||
'dashboard.fx.unavailable': 'Tasso non disponibile',
|
'dashboard.fx.unavailable': 'Tasso non disponibile',
|
||||||
'dashboard.tz.searchPlaceholder': 'Cerca fuso orario…',
|
'dashboard.tz.searchPlaceholder': 'Cerca fuso orario…',
|
||||||
'dashboard.tz.empty': 'Ancora nessun altro fuso orario — aggiungine uno con +',
|
'dashboard.tz.empty':
|
||||||
|
'Ancora nessun altro fuso orario — aggiungine uno con +',
|
||||||
'dashboard.upcoming.title': 'Prossime prenotazioni',
|
'dashboard.upcoming.title': 'Prossime prenotazioni',
|
||||||
'dashboard.upcoming.empty': 'Niente ancora prenotato.',
|
'dashboard.upcoming.empty': 'Niente ancora prenotato.',
|
||||||
'dashboard.confirm.copy.title': 'Copiare questo viaggio?',
|
'dashboard.confirm.copy.title': 'Copiare questo viaggio?',
|
||||||
|
|||||||
@@ -153,14 +153,14 @@ const dashboard: TranslationStrings = {
|
|||||||
'dashboard.confirm.copy.wontCopy': 'Wordt niet gekopieerd',
|
'dashboard.confirm.copy.wontCopy': 'Wordt niet gekopieerd',
|
||||||
'dashboard.confirm.copy.wont1': 'Medewerkers & ledentoewijzingen',
|
'dashboard.confirm.copy.wont1': 'Medewerkers & ledentoewijzingen',
|
||||||
'dashboard.confirm.copy.wont2': 'Gedeelde notities, peilingen & berichten',
|
'dashboard.confirm.copy.wont2': 'Gedeelde notities, peilingen & berichten',
|
||||||
'dashboard.confirm.copy.wont3': 'Bestanden & foto\'s',
|
'dashboard.confirm.copy.wont3': "Bestanden & foto's",
|
||||||
'dashboard.confirm.copy.wont4': 'Deeltokens',
|
'dashboard.confirm.copy.wont4': 'Deeltokens',
|
||||||
'dashboard.confirm.copy.confirm': 'Reis kopiëren',
|
'dashboard.confirm.copy.confirm': 'Reis kopiëren',
|
||||||
'dashboard.aria.toggleView': 'Weergave wisselen',
|
'dashboard.aria.toggleView': 'Weergave wisselen',
|
||||||
'dashboard.aria.filter': 'Filter',
|
'dashboard.aria.filter': 'Filter',
|
||||||
'dashboard.aria.duplicate': 'Dupliceren',
|
'dashboard.aria.duplicate': 'Dupliceren',
|
||||||
'dashboard.aria.refreshRates': 'Koersen vernieuwen',
|
'dashboard.aria.refreshRates': 'Koersen vernieuwen',
|
||||||
'dashboard.aria.swapCurrencies': 'Valuta\'s omwisselen',
|
'dashboard.aria.swapCurrencies': "Valuta's omwisselen",
|
||||||
'dashboard.aria.addTimezone': 'Tijdzone toevoegen',
|
'dashboard.aria.addTimezone': 'Tijdzone toevoegen',
|
||||||
'dashboard.aria.removeTimezone': '{city} verwijderen',
|
'dashboard.aria.removeTimezone': '{city} verwijderen',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -153,7 +153,8 @@ const dashboard: TranslationStrings = {
|
|||||||
'dashboard.fx.to': 'У',
|
'dashboard.fx.to': 'У',
|
||||||
'dashboard.fx.unavailable': 'Курс недоступний',
|
'dashboard.fx.unavailable': 'Курс недоступний',
|
||||||
'dashboard.tz.searchPlaceholder': 'Пошук часового поясу…',
|
'dashboard.tz.searchPlaceholder': 'Пошук часового поясу…',
|
||||||
'dashboard.tz.empty': 'Інших часових поясів поки немає — додайте за допомогою +',
|
'dashboard.tz.empty':
|
||||||
|
'Інших часових поясів поки немає — додайте за допомогою +',
|
||||||
'dashboard.upcoming.title': 'Найближчі бронювання',
|
'dashboard.upcoming.title': 'Найближчі бронювання',
|
||||||
'dashboard.upcoming.empty': 'Поки нічого не заброньовано.',
|
'dashboard.upcoming.empty': 'Поки нічого не заброньовано.',
|
||||||
'dashboard.aria.toggleView': 'Перемкнути вигляд',
|
'dashboard.aria.toggleView': 'Перемкнути вигляд',
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
import {
|
||||||
journeyCreateRequestSchema,
|
journeyCreateRequestSchema,
|
||||||
journeyAddTripRequestSchema,
|
journeyAddTripRequestSchema,
|
||||||
@@ -8,48 +7,92 @@ import {
|
|||||||
journeyShareLinkRequestSchema,
|
journeyShareLinkRequestSchema,
|
||||||
} from './journey.schema';
|
} from './journey.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('journeyCreateRequestSchema', () => {
|
describe('journeyCreateRequestSchema', () => {
|
||||||
it('requires a title; subtitle + trip_ids optional', () => {
|
it('requires a title; subtitle + trip_ids optional', () => {
|
||||||
expect(journeyCreateRequestSchema.safeParse({ title: 'Trip of a lifetime' }).success).toBe(true);
|
expect(
|
||||||
expect(journeyCreateRequestSchema.safeParse({ title: 'X', trip_ids: [1, '2'] }).success).toBe(true);
|
journeyCreateRequestSchema.safeParse({ title: 'Trip of a lifetime' })
|
||||||
expect(journeyCreateRequestSchema.safeParse({ subtitle: 'no title' }).success).toBe(false);
|
.success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
journeyCreateRequestSchema.safeParse({ title: 'X', trip_ids: [1, '2'] })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
journeyCreateRequestSchema.safeParse({ subtitle: 'no title' }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('journeyAddTripRequestSchema', () => {
|
describe('journeyAddTripRequestSchema', () => {
|
||||||
it('requires a trip_id (string or number)', () => {
|
it('requires a trip_id (string or number)', () => {
|
||||||
expect(journeyAddTripRequestSchema.safeParse({ trip_id: 5 }).success).toBe(true);
|
expect(journeyAddTripRequestSchema.safeParse({ trip_id: 5 }).success).toBe(
|
||||||
expect(journeyAddTripRequestSchema.safeParse({ trip_id: '5' }).success).toBe(true);
|
true,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
journeyAddTripRequestSchema.safeParse({ trip_id: '5' }).success,
|
||||||
|
).toBe(true);
|
||||||
expect(journeyAddTripRequestSchema.safeParse({}).success).toBe(false);
|
expect(journeyAddTripRequestSchema.safeParse({}).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('journeyReorderEntriesRequestSchema', () => {
|
describe('journeyReorderEntriesRequestSchema', () => {
|
||||||
it('requires a non-empty orderedIds array', () => {
|
it('requires a non-empty orderedIds array', () => {
|
||||||
expect(journeyReorderEntriesRequestSchema.safeParse({ orderedIds: [3, 1, 2] }).success).toBe(true);
|
expect(
|
||||||
expect(journeyReorderEntriesRequestSchema.safeParse({ orderedIds: [] }).success).toBe(false);
|
journeyReorderEntriesRequestSchema.safeParse({ orderedIds: [3, 1, 2] })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
journeyReorderEntriesRequestSchema.safeParse({ orderedIds: [] }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('journeyContributorRequestSchema', () => {
|
describe('journeyContributorRequestSchema', () => {
|
||||||
it('requires user_id; role limited to editor/viewer', () => {
|
it('requires user_id; role limited to editor/viewer', () => {
|
||||||
expect(journeyContributorRequestSchema.safeParse({ user_id: 2 }).success).toBe(true);
|
expect(
|
||||||
expect(journeyContributorRequestSchema.safeParse({ user_id: 2, role: 'editor' }).success).toBe(true);
|
journeyContributorRequestSchema.safeParse({ user_id: 2 }).success,
|
||||||
expect(journeyContributorRequestSchema.safeParse({ user_id: 2, role: 'admin' }).success).toBe(false);
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
journeyContributorRequestSchema.safeParse({ user_id: 2, role: 'editor' })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
journeyContributorRequestSchema.safeParse({ user_id: 2, role: 'admin' })
|
||||||
|
.success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('journeyProviderPhotosRequestSchema', () => {
|
describe('journeyProviderPhotosRequestSchema', () => {
|
||||||
it('requires a provider; accepts single asset_id or a batch', () => {
|
it('requires a provider; accepts single asset_id or a batch', () => {
|
||||||
expect(journeyProviderPhotosRequestSchema.safeParse({ provider: 'immich', asset_id: 'a1' }).success).toBe(true);
|
expect(
|
||||||
expect(journeyProviderPhotosRequestSchema.safeParse({ provider: 'immich', asset_ids: ['a1', 'a2'] }).success).toBe(true);
|
journeyProviderPhotosRequestSchema.safeParse({
|
||||||
expect(journeyProviderPhotosRequestSchema.safeParse({ asset_id: 'a1' }).success).toBe(false);
|
provider: 'immich',
|
||||||
|
asset_id: 'a1',
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
journeyProviderPhotosRequestSchema.safeParse({
|
||||||
|
provider: 'immich',
|
||||||
|
asset_ids: ['a1', 'a2'],
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
journeyProviderPhotosRequestSchema.safeParse({ asset_id: 'a1' }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('journeyShareLinkRequestSchema', () => {
|
describe('journeyShareLinkRequestSchema', () => {
|
||||||
it('accepts optional share toggles', () => {
|
it('accepts optional share toggles', () => {
|
||||||
expect(journeyShareLinkRequestSchema.safeParse({ share_timeline: true, share_gallery: false }).success).toBe(true);
|
expect(
|
||||||
|
journeyShareLinkRequestSchema.safeParse({
|
||||||
|
share_timeline: true,
|
||||||
|
share_gallery: false,
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
expect(journeyShareLinkRequestSchema.safeParse({}).success).toBe(true);
|
expect(journeyShareLinkRequestSchema.safeParse({}).success).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,13 +28,17 @@ export type JourneyAddTripRequest = z.infer<typeof journeyAddTripRequestSchema>;
|
|||||||
export const journeyReorderEntriesRequestSchema = z.object({
|
export const journeyReorderEntriesRequestSchema = z.object({
|
||||||
orderedIds: z.array(z.union([z.string(), z.number()])).min(1),
|
orderedIds: z.array(z.union([z.string(), z.number()])).min(1),
|
||||||
});
|
});
|
||||||
export type JourneyReorderEntriesRequest = z.infer<typeof journeyReorderEntriesRequestSchema>;
|
export type JourneyReorderEntriesRequest = z.infer<
|
||||||
|
typeof journeyReorderEntriesRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const journeyContributorRequestSchema = z.object({
|
export const journeyContributorRequestSchema = z.object({
|
||||||
user_id: z.union([z.string(), z.number()]),
|
user_id: z.union([z.string(), z.number()]),
|
||||||
role: z.enum(['editor', 'viewer']).optional(),
|
role: z.enum(['editor', 'viewer']).optional(),
|
||||||
});
|
});
|
||||||
export type JourneyContributorRequest = z.infer<typeof journeyContributorRequestSchema>;
|
export type JourneyContributorRequest = z.infer<
|
||||||
|
typeof journeyContributorRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const journeyProviderPhotosRequestSchema = z.object({
|
export const journeyProviderPhotosRequestSchema = z.object({
|
||||||
provider: z.string().min(1),
|
provider: z.string().min(1),
|
||||||
@@ -43,11 +47,15 @@ export const journeyProviderPhotosRequestSchema = z.object({
|
|||||||
caption: z.string().optional(),
|
caption: z.string().optional(),
|
||||||
passphrase: z.string().optional(),
|
passphrase: z.string().optional(),
|
||||||
});
|
});
|
||||||
export type JourneyProviderPhotosRequest = z.infer<typeof journeyProviderPhotosRequestSchema>;
|
export type JourneyProviderPhotosRequest = z.infer<
|
||||||
|
typeof journeyProviderPhotosRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const journeyShareLinkRequestSchema = z.object({
|
export const journeyShareLinkRequestSchema = z.object({
|
||||||
share_timeline: z.boolean().optional(),
|
share_timeline: z.boolean().optional(),
|
||||||
share_gallery: z.boolean().optional(),
|
share_gallery: z.boolean().optional(),
|
||||||
share_map: z.boolean().optional(),
|
share_map: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
export type JourneyShareLinkRequest = z.infer<typeof journeyShareLinkRequestSchema>;
|
export type JourneyShareLinkRequest = z.infer<
|
||||||
|
typeof journeyShareLinkRequestSchema
|
||||||
|
>;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
import {
|
||||||
mapsSearchRequestSchema,
|
mapsSearchRequestSchema,
|
||||||
mapsAutocompleteRequestSchema,
|
mapsAutocompleteRequestSchema,
|
||||||
@@ -6,34 +5,58 @@ import {
|
|||||||
mapsResolveUrlRequestSchema,
|
mapsResolveUrlRequestSchema,
|
||||||
} from './maps.schema';
|
} from './maps.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('mapsSearchRequestSchema', () => {
|
describe('mapsSearchRequestSchema', () => {
|
||||||
it('requires a non-empty query', () => {
|
it('requires a non-empty query', () => {
|
||||||
expect(mapsSearchRequestSchema.safeParse({ query: 'berlin' }).success).toBe(true);
|
expect(mapsSearchRequestSchema.safeParse({ query: 'berlin' }).success).toBe(
|
||||||
expect(mapsSearchRequestSchema.safeParse({ query: '' }).success).toBe(false);
|
true,
|
||||||
|
);
|
||||||
|
expect(mapsSearchRequestSchema.safeParse({ query: '' }).success).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
expect(mapsSearchRequestSchema.safeParse({}).success).toBe(false);
|
expect(mapsSearchRequestSchema.safeParse({}).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mapsAutocompleteRequestSchema', () => {
|
describe('mapsAutocompleteRequestSchema', () => {
|
||||||
it('caps input at 200 chars and allows an optional locationBias', () => {
|
it('caps input at 200 chars and allows an optional locationBias', () => {
|
||||||
expect(mapsAutocompleteRequestSchema.safeParse({ input: 'be' }).success).toBe(true);
|
expect(
|
||||||
expect(mapsAutocompleteRequestSchema.safeParse({ input: 'x'.repeat(201) }).success).toBe(false);
|
mapsAutocompleteRequestSchema.safeParse({ input: 'be' }).success,
|
||||||
expect(mapsAutocompleteRequestSchema.safeParse({
|
).toBe(true);
|
||||||
input: 'be', locationBias: { low: { lat: 1, lng: 2 }, high: { lat: 3, lng: 4 } },
|
expect(
|
||||||
}).success).toBe(true);
|
mapsAutocompleteRequestSchema.safeParse({ input: 'x'.repeat(201) })
|
||||||
|
.success,
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
mapsAutocompleteRequestSchema.safeParse({
|
||||||
|
input: 'be',
|
||||||
|
locationBias: { low: { lat: 1, lng: 2 }, high: { lat: 3, lng: 4 } },
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mapsReverseQuerySchema', () => {
|
describe('mapsReverseQuerySchema', () => {
|
||||||
it('requires lat and lng as strings (the route parses them downstream)', () => {
|
it('requires lat and lng as strings (the route parses them downstream)', () => {
|
||||||
expect(mapsReverseQuerySchema.safeParse({ lat: '52.5', lng: '13.4' }).success).toBe(true);
|
expect(
|
||||||
expect(mapsReverseQuerySchema.safeParse({ lat: '52.5' }).success).toBe(false);
|
mapsReverseQuerySchema.safeParse({ lat: '52.5', lng: '13.4' }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(mapsReverseQuerySchema.safeParse({ lat: '52.5' }).success).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mapsResolveUrlRequestSchema', () => {
|
describe('mapsResolveUrlRequestSchema', () => {
|
||||||
it('requires a non-empty url', () => {
|
it('requires a non-empty url', () => {
|
||||||
expect(mapsResolveUrlRequestSchema.safeParse({ url: 'https://maps.app.goo.gl/x' }).success).toBe(true);
|
expect(
|
||||||
expect(mapsResolveUrlRequestSchema.safeParse({ url: '' }).success).toBe(false);
|
mapsResolveUrlRequestSchema.safeParse({
|
||||||
|
url: 'https://maps.app.goo.gl/x',
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(mapsResolveUrlRequestSchema.safeParse({ url: '' }).success).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ export const mapsAutocompleteRequestSchema = z.object({
|
|||||||
lang: z.string().optional(),
|
lang: z.string().optional(),
|
||||||
locationBias: z.object({ low: latLng, high: latLng }).optional(),
|
locationBias: z.object({ low: latLng, high: latLng }).optional(),
|
||||||
});
|
});
|
||||||
export type MapsAutocompleteRequest = z.infer<typeof mapsAutocompleteRequestSchema>;
|
export type MapsAutocompleteRequest = z.infer<
|
||||||
|
typeof mapsAutocompleteRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const mapsReverseQuerySchema = z.object({
|
export const mapsReverseQuerySchema = z.object({
|
||||||
lat: z.string().min(1),
|
lat: z.string().min(1),
|
||||||
@@ -59,13 +61,17 @@ export const mapsAutocompleteResultSchema = z.object({
|
|||||||
suggestions: z.array(mapsAutocompleteSuggestionSchema),
|
suggestions: z.array(mapsAutocompleteSuggestionSchema),
|
||||||
source: z.string(),
|
source: z.string(),
|
||||||
});
|
});
|
||||||
export type MapsAutocompleteResult = z.infer<typeof mapsAutocompleteResultSchema>;
|
export type MapsAutocompleteResult = z.infer<
|
||||||
|
typeof mapsAutocompleteResultSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const mapsPlaceDetailsResultSchema = z.object({
|
export const mapsPlaceDetailsResultSchema = z.object({
|
||||||
place: placeRecord.nullable(),
|
place: placeRecord.nullable(),
|
||||||
disabled: z.boolean().optional(),
|
disabled: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
export type MapsPlaceDetailsResult = z.infer<typeof mapsPlaceDetailsResultSchema>;
|
export type MapsPlaceDetailsResult = z.infer<
|
||||||
|
typeof mapsPlaceDetailsResultSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const mapsPlacePhotoResultSchema = z.object({
|
export const mapsPlacePhotoResultSchema = z.object({
|
||||||
photoUrl: z.string().nullable(),
|
photoUrl: z.string().nullable(),
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
import {
|
||||||
preferencesUpdateRequestSchema,
|
preferencesUpdateRequestSchema,
|
||||||
notificationRespondRequestSchema,
|
notificationRespondRequestSchema,
|
||||||
@@ -6,31 +5,55 @@ import {
|
|||||||
inAppListResultSchema,
|
inAppListResultSchema,
|
||||||
} from './notification.schema';
|
} from './notification.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('preferencesUpdateRequestSchema', () => {
|
describe('preferencesUpdateRequestSchema', () => {
|
||||||
it('accepts a nested event/channel/enabled matrix', () => {
|
it('accepts a nested event/channel/enabled matrix', () => {
|
||||||
expect(preferencesUpdateRequestSchema.safeParse({ trip_invite: { inapp: true, email: false } }).success).toBe(true);
|
expect(
|
||||||
expect(preferencesUpdateRequestSchema.safeParse({ trip_invite: { inapp: 'yes' } }).success).toBe(false);
|
preferencesUpdateRequestSchema.safeParse({
|
||||||
|
trip_invite: { inapp: true, email: false },
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
preferencesUpdateRequestSchema.safeParse({
|
||||||
|
trip_invite: { inapp: 'yes' },
|
||||||
|
}).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('notificationRespondRequestSchema', () => {
|
describe('notificationRespondRequestSchema', () => {
|
||||||
it('only accepts positive/negative', () => {
|
it('only accepts positive/negative', () => {
|
||||||
expect(notificationRespondRequestSchema.safeParse({ response: 'positive' }).success).toBe(true);
|
expect(
|
||||||
expect(notificationRespondRequestSchema.safeParse({ response: 'maybe' }).success).toBe(false);
|
notificationRespondRequestSchema.safeParse({ response: 'positive' })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
notificationRespondRequestSchema.safeParse({ response: 'maybe' }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('channelTestResultSchema', () => {
|
describe('channelTestResultSchema', () => {
|
||||||
it('accepts a success result and an error result', () => {
|
it('accepts a success result and an error result', () => {
|
||||||
expect(channelTestResultSchema.safeParse({ success: true }).success).toBe(true);
|
expect(channelTestResultSchema.safeParse({ success: true }).success).toBe(
|
||||||
expect(channelTestResultSchema.safeParse({ success: false, error: 'SMTP down' }).success).toBe(true);
|
true,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
channelTestResultSchema.safeParse({ success: false, error: 'SMTP down' })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('inAppListResultSchema', () => {
|
describe('inAppListResultSchema', () => {
|
||||||
it('accepts the list envelope with open notification rows', () => {
|
it('accepts the list envelope with open notification rows', () => {
|
||||||
expect(inAppListResultSchema.safeParse({
|
expect(
|
||||||
notifications: [{ id: 1, type: 'info', anything: 'goes' }], total: 1, unread_count: 0,
|
inAppListResultSchema.safeParse({
|
||||||
}).success).toBe(true);
|
notifications: [{ id: 1, type: 'info', anything: 'goes' }],
|
||||||
|
total: 1,
|
||||||
|
unread_count: 0,
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,10 +18,14 @@ export const preferencesUpdateRequestSchema = z.record(
|
|||||||
z.string(),
|
z.string(),
|
||||||
z.record(z.string(), z.boolean()),
|
z.record(z.string(), z.boolean()),
|
||||||
);
|
);
|
||||||
export type PreferencesUpdateRequest = z.infer<typeof preferencesUpdateRequestSchema>;
|
export type PreferencesUpdateRequest = z.infer<
|
||||||
|
typeof preferencesUpdateRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const testSmtpRequestSchema = z.object({ email: z.string().optional() });
|
export const testSmtpRequestSchema = z.object({ email: z.string().optional() });
|
||||||
export const testWebhookRequestSchema = z.object({ url: z.string().optional() });
|
export const testWebhookRequestSchema = z.object({
|
||||||
|
url: z.string().optional(),
|
||||||
|
});
|
||||||
export const testNtfyRequestSchema = z.object({
|
export const testNtfyRequestSchema = z.object({
|
||||||
topic: z.string().optional(),
|
topic: z.string().optional(),
|
||||||
server: z.string().optional(),
|
server: z.string().optional(),
|
||||||
@@ -39,7 +43,9 @@ export type ChannelTestResult = z.infer<typeof channelTestResultSchema>;
|
|||||||
export const notificationRespondRequestSchema = z.object({
|
export const notificationRespondRequestSchema = z.object({
|
||||||
response: z.enum(['positive', 'negative']),
|
response: z.enum(['positive', 'negative']),
|
||||||
});
|
});
|
||||||
export type NotificationRespondRequest = z.infer<typeof notificationRespondRequestSchema>;
|
export type NotificationRespondRequest = z.infer<
|
||||||
|
typeof notificationRespondRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
/** A single in-app notification row (DB-shaped; kept open). */
|
/** A single in-app notification row (DB-shaped; kept open). */
|
||||||
export const notificationRowSchema = z.record(z.string(), z.unknown());
|
export const notificationRowSchema = z.record(z.string(), z.unknown());
|
||||||
|
|||||||
@@ -1,25 +1,71 @@
|
|||||||
|
import {
|
||||||
|
oauthTokenRequestSchema,
|
||||||
|
oauthConsentRequestSchema,
|
||||||
|
oauthClientCreateRequestSchema,
|
||||||
|
} from './oauth.schema';
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { oauthTokenRequestSchema, oauthConsentRequestSchema, oauthClientCreateRequestSchema } from './oauth.schema';
|
|
||||||
|
|
||||||
describe('oauthTokenRequestSchema', () => {
|
describe('oauthTokenRequestSchema', () => {
|
||||||
it('is permissive across grant types and passes extras through', () => {
|
it('is permissive across grant types and passes extras through', () => {
|
||||||
expect(oauthTokenRequestSchema.safeParse({ grant_type: 'authorization_code', client_id: 'c', code: 'x', redirect_uri: 'u', code_verifier: 'v' }).success).toBe(true);
|
expect(
|
||||||
expect(oauthTokenRequestSchema.safeParse({ grant_type: 'client_credentials', client_id: 'c', client_secret: 's', scope: 'a b' }).success).toBe(true);
|
oauthTokenRequestSchema.safeParse({
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
client_id: 'c',
|
||||||
|
code: 'x',
|
||||||
|
redirect_uri: 'u',
|
||||||
|
code_verifier: 'v',
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
oauthTokenRequestSchema.safeParse({
|
||||||
|
grant_type: 'client_credentials',
|
||||||
|
client_id: 'c',
|
||||||
|
client_secret: 's',
|
||||||
|
scope: 'a b',
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
expect(oauthTokenRequestSchema.safeParse({}).success).toBe(true);
|
expect(oauthTokenRequestSchema.safeParse({}).success).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('oauthConsentRequestSchema', () => {
|
describe('oauthConsentRequestSchema', () => {
|
||||||
it('requires the PKCE consent fields + approved flag', () => {
|
it('requires the PKCE consent fields + approved flag', () => {
|
||||||
expect(oauthConsentRequestSchema.safeParse({ client_id: 'c', redirect_uri: 'u', scope: 's', code_challenge: 'cc', code_challenge_method: 'S256', approved: true }).success).toBe(true);
|
expect(
|
||||||
expect(oauthConsentRequestSchema.safeParse({ client_id: 'c', redirect_uri: 'u', scope: 's', code_challenge: 'cc', code_challenge_method: 'S256' }).success).toBe(false);
|
oauthConsentRequestSchema.safeParse({
|
||||||
|
client_id: 'c',
|
||||||
|
redirect_uri: 'u',
|
||||||
|
scope: 's',
|
||||||
|
code_challenge: 'cc',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
approved: true,
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
oauthConsentRequestSchema.safeParse({
|
||||||
|
client_id: 'c',
|
||||||
|
redirect_uri: 'u',
|
||||||
|
scope: 's',
|
||||||
|
code_challenge: 'cc',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
}).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('oauthClientCreateRequestSchema', () => {
|
describe('oauthClientCreateRequestSchema', () => {
|
||||||
it('requires name + allowed_scopes', () => {
|
it('requires name + allowed_scopes', () => {
|
||||||
expect(oauthClientCreateRequestSchema.safeParse({ name: 'CLI', allowed_scopes: ['trips:read'] }).success).toBe(true);
|
expect(
|
||||||
expect(oauthClientCreateRequestSchema.safeParse({ name: 'CLI' }).success).toBe(false);
|
oauthClientCreateRequestSchema.safeParse({
|
||||||
expect(oauthClientCreateRequestSchema.safeParse({ allowed_scopes: [] }).success).toBe(false);
|
name: 'CLI',
|
||||||
|
allowed_scopes: ['trips:read'],
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
oauthClientCreateRequestSchema.safeParse({ name: 'CLI' }).success,
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
oauthClientCreateRequestSchema.safeParse({ allowed_scopes: [] }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,4 +41,6 @@ export const oauthClientCreateRequestSchema = z.object({
|
|||||||
allowed_scopes: z.array(z.string()),
|
allowed_scopes: z.array(z.string()),
|
||||||
allows_client_credentials: z.boolean().optional(),
|
allows_client_credentials: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
export type OauthClientCreateRequest = z.infer<typeof oauthClientCreateRequestSchema>;
|
export type OauthClientCreateRequest = z.infer<
|
||||||
|
typeof oauthClientCreateRequestSchema
|
||||||
|
>;
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
|
import {
|
||||||
|
oidcCallbackQuerySchema,
|
||||||
|
oidcExchangeQuerySchema,
|
||||||
|
} from './oidc.schema';
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { oidcCallbackQuerySchema, oidcExchangeQuerySchema } from './oidc.schema';
|
|
||||||
|
|
||||||
describe('oidcCallbackQuerySchema', () => {
|
describe('oidcCallbackQuerySchema', () => {
|
||||||
it('accepts code+state, an error, or nothing (all optional)', () => {
|
it('accepts code+state, an error, or nothing (all optional)', () => {
|
||||||
expect(oidcCallbackQuerySchema.safeParse({ code: 'c', state: 's' }).success).toBe(true);
|
expect(
|
||||||
expect(oidcCallbackQuerySchema.safeParse({ error: 'access_denied' }).success).toBe(true);
|
oidcCallbackQuerySchema.safeParse({ code: 'c', state: 's' }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
oidcCallbackQuerySchema.safeParse({ error: 'access_denied' }).success,
|
||||||
|
).toBe(true);
|
||||||
expect(oidcCallbackQuerySchema.safeParse({}).success).toBe(true);
|
expect(oidcCallbackQuerySchema.safeParse({}).success).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
import {
|
||||||
packingCreateItemRequestSchema,
|
packingCreateItemRequestSchema,
|
||||||
packingImportRequestSchema,
|
packingImportRequestSchema,
|
||||||
@@ -6,30 +5,52 @@ import {
|
|||||||
packingSaveTemplateRequestSchema,
|
packingSaveTemplateRequestSchema,
|
||||||
} from './packing.schema';
|
} from './packing.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('packingCreateItemRequestSchema', () => {
|
describe('packingCreateItemRequestSchema', () => {
|
||||||
it('requires a non-empty name; category/checked optional', () => {
|
it('requires a non-empty name; category/checked optional', () => {
|
||||||
expect(packingCreateItemRequestSchema.safeParse({ name: 'Socks' }).success).toBe(true);
|
expect(
|
||||||
expect(packingCreateItemRequestSchema.safeParse({ name: 'Socks', category: 'Clothes', checked: true }).success).toBe(true);
|
packingCreateItemRequestSchema.safeParse({ name: 'Socks' }).success,
|
||||||
expect(packingCreateItemRequestSchema.safeParse({ name: '' }).success).toBe(false);
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
packingCreateItemRequestSchema.safeParse({
|
||||||
|
name: 'Socks',
|
||||||
|
category: 'Clothes',
|
||||||
|
checked: true,
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(packingCreateItemRequestSchema.safeParse({ name: '' }).success).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('packingImportRequestSchema', () => {
|
describe('packingImportRequestSchema', () => {
|
||||||
it('accepts an array of open item rows', () => {
|
it('accepts an array of open item rows', () => {
|
||||||
expect(packingImportRequestSchema.safeParse({ items: [{ name: 'a' }, { name: 'b', anything: 1 }] }).success).toBe(true);
|
expect(
|
||||||
|
packingImportRequestSchema.safeParse({
|
||||||
|
items: [{ name: 'a' }, { name: 'b', anything: 1 }],
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('packingCreateBagRequestSchema', () => {
|
describe('packingCreateBagRequestSchema', () => {
|
||||||
it('requires a name', () => {
|
it('requires a name', () => {
|
||||||
expect(packingCreateBagRequestSchema.safeParse({ name: 'Carry-on' }).success).toBe(true);
|
expect(
|
||||||
|
packingCreateBagRequestSchema.safeParse({ name: 'Carry-on' }).success,
|
||||||
|
).toBe(true);
|
||||||
expect(packingCreateBagRequestSchema.safeParse({}).success).toBe(false);
|
expect(packingCreateBagRequestSchema.safeParse({}).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('packingSaveTemplateRequestSchema', () => {
|
describe('packingSaveTemplateRequestSchema', () => {
|
||||||
it('requires a name', () => {
|
it('requires a name', () => {
|
||||||
expect(packingSaveTemplateRequestSchema.safeParse({ name: 'Summer' }).success).toBe(true);
|
expect(
|
||||||
expect(packingSaveTemplateRequestSchema.safeParse({ name: '' }).success).toBe(false);
|
packingSaveTemplateRequestSchema.safeParse({ name: 'Summer' }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
packingSaveTemplateRequestSchema.safeParse({ name: '' }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -68,7 +68,9 @@ export const packingCreateItemRequestSchema = z.object({
|
|||||||
category: z.string().optional(),
|
category: z.string().optional(),
|
||||||
checked: z.boolean().optional(),
|
checked: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
export type PackingCreateItemRequest = z.infer<typeof packingCreateItemRequestSchema>;
|
export type PackingCreateItemRequest = z.infer<
|
||||||
|
typeof packingCreateItemRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const packingUpdateItemRequestSchema = z.object({
|
export const packingUpdateItemRequestSchema = z.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
@@ -78,7 +80,9 @@ export const packingUpdateItemRequestSchema = z.object({
|
|||||||
bag_id: z.number().nullable().optional(),
|
bag_id: z.number().nullable().optional(),
|
||||||
quantity: z.number().optional(),
|
quantity: z.number().optional(),
|
||||||
});
|
});
|
||||||
export type PackingUpdateItemRequest = z.infer<typeof packingUpdateItemRequestSchema>;
|
export type PackingUpdateItemRequest = z.infer<
|
||||||
|
typeof packingUpdateItemRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const packingImportRequestSchema = z.object({
|
export const packingImportRequestSchema = z.object({
|
||||||
items: z.array(open),
|
items: z.array(open),
|
||||||
@@ -94,7 +98,9 @@ export const packingCreateBagRequestSchema = z.object({
|
|||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
color: z.string().optional(),
|
color: z.string().optional(),
|
||||||
});
|
});
|
||||||
export type PackingCreateBagRequest = z.infer<typeof packingCreateBagRequestSchema>;
|
export type PackingCreateBagRequest = z.infer<
|
||||||
|
typeof packingCreateBagRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const packingUpdateBagRequestSchema = z.object({
|
export const packingUpdateBagRequestSchema = z.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
@@ -102,19 +108,27 @@ export const packingUpdateBagRequestSchema = z.object({
|
|||||||
weight_limit_grams: z.number().nullable().optional(),
|
weight_limit_grams: z.number().nullable().optional(),
|
||||||
user_id: z.number().nullable().optional(),
|
user_id: z.number().nullable().optional(),
|
||||||
});
|
});
|
||||||
export type PackingUpdateBagRequest = z.infer<typeof packingUpdateBagRequestSchema>;
|
export type PackingUpdateBagRequest = z.infer<
|
||||||
|
typeof packingUpdateBagRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const packingBagMembersRequestSchema = z.object({
|
export const packingBagMembersRequestSchema = z.object({
|
||||||
user_ids: z.array(z.number()),
|
user_ids: z.array(z.number()),
|
||||||
});
|
});
|
||||||
export type PackingBagMembersRequest = z.infer<typeof packingBagMembersRequestSchema>;
|
export type PackingBagMembersRequest = z.infer<
|
||||||
|
typeof packingBagMembersRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const packingSaveTemplateRequestSchema = z.object({
|
export const packingSaveTemplateRequestSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
});
|
});
|
||||||
export type PackingSaveTemplateRequest = z.infer<typeof packingSaveTemplateRequestSchema>;
|
export type PackingSaveTemplateRequest = z.infer<
|
||||||
|
typeof packingSaveTemplateRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const packingCategoryAssigneesRequestSchema = z.object({
|
export const packingCategoryAssigneesRequestSchema = z.object({
|
||||||
user_ids: z.array(z.number()),
|
user_ids: z.array(z.number()),
|
||||||
});
|
});
|
||||||
export type PackingCategoryAssigneesRequest = z.infer<typeof packingCategoryAssigneesRequestSchema>;
|
export type PackingCategoryAssigneesRequest = z.infer<
|
||||||
|
typeof packingCategoryAssigneesRequestSchema
|
||||||
|
>;
|
||||||
|
|||||||
@@ -1,27 +1,43 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
import {
|
||||||
placeCreateRequestSchema,
|
placeCreateRequestSchema,
|
||||||
placeBulkDeleteRequestSchema,
|
placeBulkDeleteRequestSchema,
|
||||||
placeImportListRequestSchema,
|
placeImportListRequestSchema,
|
||||||
} from './place.schema';
|
} from './place.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('placeCreateRequestSchema', () => {
|
describe('placeCreateRequestSchema', () => {
|
||||||
it('requires a name and keeps the other place fields open', () => {
|
it('requires a name and keeps the other place fields open', () => {
|
||||||
expect(placeCreateRequestSchema.safeParse({ name: 'Spot', lat: 1, lng: 2, anything: true }).success).toBe(true);
|
expect(
|
||||||
|
placeCreateRequestSchema.safeParse({
|
||||||
|
name: 'Spot',
|
||||||
|
lat: 1,
|
||||||
|
lng: 2,
|
||||||
|
anything: true,
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
expect(placeCreateRequestSchema.safeParse({ lat: 1 }).success).toBe(false);
|
expect(placeCreateRequestSchema.safeParse({ lat: 1 }).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('placeBulkDeleteRequestSchema', () => {
|
describe('placeBulkDeleteRequestSchema', () => {
|
||||||
it('requires a numeric ids array', () => {
|
it('requires a numeric ids array', () => {
|
||||||
expect(placeBulkDeleteRequestSchema.safeParse({ ids: [1, 2] }).success).toBe(true);
|
expect(
|
||||||
expect(placeBulkDeleteRequestSchema.safeParse({ ids: ['a'] }).success).toBe(false);
|
placeBulkDeleteRequestSchema.safeParse({ ids: [1, 2] }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(placeBulkDeleteRequestSchema.safeParse({ ids: ['a'] }).success).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('placeImportListRequestSchema', () => {
|
describe('placeImportListRequestSchema', () => {
|
||||||
it('requires a non-empty url', () => {
|
it('requires a non-empty url', () => {
|
||||||
expect(placeImportListRequestSchema.safeParse({ url: 'http://x' }).success).toBe(true);
|
expect(
|
||||||
expect(placeImportListRequestSchema.safeParse({ url: '' }).success).toBe(false);
|
placeImportListRequestSchema.safeParse({ url: 'http://x' }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(placeImportListRequestSchema.safeParse({ url: '' }).success).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
import { categorySchema } from '../category/category.schema';
|
|
||||||
import { tagSchema } from '../tag/tag.schema';
|
import { tagSchema } from '../tag/tag.schema';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Place API contract — single source of truth for the /api/trips/:tripId/places
|
* Place API contract — single source of truth for the /api/trips/:tripId/places
|
||||||
* endpoints (place pool CRUD, GPX/map/list imports, image search, bulk delete).
|
* endpoints (place pool CRUD, GPX/map/list imports, image search, bulk delete).
|
||||||
@@ -100,7 +100,9 @@ export const assignmentPlaceSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type AssignmentPlace = z.infer<typeof assignmentPlaceSchema>;
|
export type AssignmentPlace = z.infer<typeof assignmentPlaceSchema>;
|
||||||
|
|
||||||
export const placeCreateRequestSchema = open.and(z.object({ name: z.string().min(1) }));
|
export const placeCreateRequestSchema = open.and(
|
||||||
|
z.object({ name: z.string().min(1) }),
|
||||||
|
);
|
||||||
export type PlaceCreateRequest = z.infer<typeof placeCreateRequestSchema>;
|
export type PlaceCreateRequest = z.infer<typeof placeCreateRequestSchema>;
|
||||||
|
|
||||||
export const placeUpdateRequestSchema = open;
|
export const placeUpdateRequestSchema = open;
|
||||||
@@ -109,12 +111,16 @@ export type PlaceUpdateRequest = z.infer<typeof placeUpdateRequestSchema>;
|
|||||||
export const placeBulkDeleteRequestSchema = z.object({
|
export const placeBulkDeleteRequestSchema = z.object({
|
||||||
ids: z.array(z.number()),
|
ids: z.array(z.number()),
|
||||||
});
|
});
|
||||||
export type PlaceBulkDeleteRequest = z.infer<typeof placeBulkDeleteRequestSchema>;
|
export type PlaceBulkDeleteRequest = z.infer<
|
||||||
|
typeof placeBulkDeleteRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const placeImportListRequestSchema = z.object({
|
export const placeImportListRequestSchema = z.object({
|
||||||
url: z.string().min(1),
|
url: z.string().min(1),
|
||||||
});
|
});
|
||||||
export type PlaceImportListRequest = z.infer<typeof placeImportListRequestSchema>;
|
export type PlaceImportListRequest = z.infer<
|
||||||
|
typeof placeImportListRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
/** Query filters for the place list. */
|
/** Query filters for the place list. */
|
||||||
export const placeListQuerySchema = z.object({
|
export const placeListQuerySchema = z.object({
|
||||||
|
|||||||
@@ -1,27 +1,52 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
import {
|
||||||
reservationCreateRequestSchema,
|
reservationCreateRequestSchema,
|
||||||
reservationPositionsRequestSchema,
|
reservationPositionsRequestSchema,
|
||||||
accommodationCreateRequestSchema,
|
accommodationCreateRequestSchema,
|
||||||
} from './reservation.schema';
|
} from './reservation.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('reservationCreateRequestSchema', () => {
|
describe('reservationCreateRequestSchema', () => {
|
||||||
it('requires a title and keeps the other booking fields open', () => {
|
it('requires a title and keeps the other booking fields open', () => {
|
||||||
expect(reservationCreateRequestSchema.safeParse({ title: 'Hotel', anything: 1, metadata: {} }).success).toBe(true);
|
expect(
|
||||||
expect(reservationCreateRequestSchema.safeParse({ location: 'x' }).success).toBe(false);
|
reservationCreateRequestSchema.safeParse({
|
||||||
|
title: 'Hotel',
|
||||||
|
anything: 1,
|
||||||
|
metadata: {},
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
reservationCreateRequestSchema.safeParse({ location: 'x' }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('reservationPositionsRequestSchema', () => {
|
describe('reservationPositionsRequestSchema', () => {
|
||||||
it('requires positions with id + day_plan_position', () => {
|
it('requires positions with id + day_plan_position', () => {
|
||||||
expect(reservationPositionsRequestSchema.safeParse({ positions: [{ id: 1, day_plan_position: 0 }], day_id: 3 }).success).toBe(true);
|
expect(
|
||||||
expect(reservationPositionsRequestSchema.safeParse({ positions: [{ id: 1 }] }).success).toBe(false);
|
reservationPositionsRequestSchema.safeParse({
|
||||||
|
positions: [{ id: 1, day_plan_position: 0 }],
|
||||||
|
day_id: 3,
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
reservationPositionsRequestSchema.safeParse({ positions: [{ id: 1 }] })
|
||||||
|
.success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('accommodationCreateRequestSchema', () => {
|
describe('accommodationCreateRequestSchema', () => {
|
||||||
it('requires place + start/end day; check-in/out optional', () => {
|
it('requires place + start/end day; check-in/out optional', () => {
|
||||||
expect(accommodationCreateRequestSchema.safeParse({ place_id: 2, start_day_id: 10, end_day_id: 11 }).success).toBe(true);
|
expect(
|
||||||
expect(accommodationCreateRequestSchema.safeParse({ place_id: 2 }).success).toBe(false);
|
accommodationCreateRequestSchema.safeParse({
|
||||||
|
place_id: 2,
|
||||||
|
start_day_id: 10,
|
||||||
|
end_day_id: 11,
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
accommodationCreateRequestSchema.safeParse({ place_id: 2 }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -101,17 +101,27 @@ export const accommodationSchema = z.object({
|
|||||||
export type Accommodation = z.infer<typeof accommodationSchema>;
|
export type Accommodation = z.infer<typeof accommodationSchema>;
|
||||||
|
|
||||||
/** Reservation create: title is required; the many optional fields stay open. */
|
/** Reservation create: title is required; the many optional fields stay open. */
|
||||||
export const reservationCreateRequestSchema = open.and(z.object({ title: z.string().min(1) }));
|
export const reservationCreateRequestSchema = open.and(
|
||||||
export type ReservationCreateRequest = z.infer<typeof reservationCreateRequestSchema>;
|
z.object({ title: z.string().min(1) }),
|
||||||
|
);
|
||||||
|
export type ReservationCreateRequest = z.infer<
|
||||||
|
typeof reservationCreateRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const reservationUpdateRequestSchema = open;
|
export const reservationUpdateRequestSchema = open;
|
||||||
export type ReservationUpdateRequest = z.infer<typeof reservationUpdateRequestSchema>;
|
export type ReservationUpdateRequest = z.infer<
|
||||||
|
typeof reservationUpdateRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const reservationPositionsRequestSchema = z.object({
|
export const reservationPositionsRequestSchema = z.object({
|
||||||
positions: z.array(z.object({ id: z.number(), day_plan_position: z.number() })),
|
positions: z.array(
|
||||||
|
z.object({ id: z.number(), day_plan_position: z.number() }),
|
||||||
|
),
|
||||||
day_id: z.union([z.number(), z.string()]).nullable().optional(),
|
day_id: z.union([z.number(), z.string()]).nullable().optional(),
|
||||||
});
|
});
|
||||||
export type ReservationPositionsRequest = z.infer<typeof reservationPositionsRequestSchema>;
|
export type ReservationPositionsRequest = z.infer<
|
||||||
|
typeof reservationPositionsRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const accommodationCreateRequestSchema = z.object({
|
export const accommodationCreateRequestSchema = z.object({
|
||||||
place_id: z.union([z.number(), z.string()]),
|
place_id: z.union([z.number(), z.string()]),
|
||||||
@@ -123,7 +133,11 @@ export const accommodationCreateRequestSchema = z.object({
|
|||||||
confirmation: z.string().nullable().optional(),
|
confirmation: z.string().nullable().optional(),
|
||||||
notes: z.string().nullable().optional(),
|
notes: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
export type AccommodationCreateRequest = z.infer<typeof accommodationCreateRequestSchema>;
|
export type AccommodationCreateRequest = z.infer<
|
||||||
|
typeof accommodationCreateRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const accommodationUpdateRequestSchema = open;
|
export const accommodationUpdateRequestSchema = open;
|
||||||
export type AccommodationUpdateRequest = z.infer<typeof accommodationUpdateRequestSchema>;
|
export type AccommodationUpdateRequest = z.infer<
|
||||||
|
typeof accommodationUpdateRequestSchema
|
||||||
|
>;
|
||||||
|
|||||||
@@ -1,118 +1,139 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
import {
|
||||||
import { sanitizeInlineHtml, sanitizeRichTextHtml, escapeHtml } from './sanitize'
|
sanitizeInlineHtml,
|
||||||
|
sanitizeRichTextHtml,
|
||||||
|
escapeHtml,
|
||||||
|
} from './sanitize';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('escapeHtml', () => {
|
describe('escapeHtml', () => {
|
||||||
it('escapes the five metacharacters', () => {
|
it('escapes the five metacharacters', () => {
|
||||||
expect(escapeHtml(`a & b < c > d " e ' f`)).toBe('a & b < c > d " e ' f')
|
expect(escapeHtml(`a & b < c > d " e ' f`)).toBe(
|
||||||
})
|
'a & b < c > d " e ' f',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('escapes ampersands first (no double-escape of entities)', () => {
|
it('escapes ampersands first (no double-escape of entities)', () => {
|
||||||
expect(escapeHtml('<')).toBe('&lt;')
|
expect(escapeHtml('<')).toBe('&lt;');
|
||||||
})
|
});
|
||||||
|
|
||||||
it('returns empty string for empty input', () => {
|
it('returns empty string for empty input', () => {
|
||||||
expect(escapeHtml('')).toBe('')
|
expect(escapeHtml('')).toBe('');
|
||||||
})
|
});
|
||||||
|
|
||||||
it('leaves plain ASCII text untouched', () => {
|
it('leaves plain ASCII text untouched', () => {
|
||||||
expect(escapeHtml('Paris Adventure 2026')).toBe('Paris Adventure 2026')
|
expect(escapeHtml('Paris Adventure 2026')).toBe('Paris Adventure 2026');
|
||||||
})
|
});
|
||||||
|
|
||||||
it('neutralises a script tag without sanitising', () => {
|
it('neutralises a script tag without sanitising', () => {
|
||||||
expect(escapeHtml('<script>alert(1)</script>')).toBe('<script>alert(1)</script>')
|
expect(escapeHtml('<script>alert(1)</script>')).toBe(
|
||||||
})
|
'<script>alert(1)</script>',
|
||||||
})
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('sanitizeInlineHtml', () => {
|
describe('sanitizeInlineHtml', () => {
|
||||||
it('returns empty string for empty input', () => {
|
it('returns empty string for empty input', () => {
|
||||||
expect(sanitizeInlineHtml('')).toBe('')
|
expect(sanitizeInlineHtml('')).toBe('');
|
||||||
})
|
});
|
||||||
|
|
||||||
it('preserves the allowed inline tags', () => {
|
it('preserves the allowed inline tags', () => {
|
||||||
expect(sanitizeInlineHtml('a <strong>b</strong> c')).toBe('a <strong>b</strong> c')
|
expect(sanitizeInlineHtml('a <strong>b</strong> c')).toBe(
|
||||||
expect(sanitizeInlineHtml('<em>x</em>')).toBe('<em>x</em>')
|
'a <strong>b</strong> c',
|
||||||
})
|
);
|
||||||
|
expect(sanitizeInlineHtml('<em>x</em>')).toBe('<em>x</em>');
|
||||||
|
});
|
||||||
|
|
||||||
it('strips <script> entirely', () => {
|
it('strips <script> entirely', () => {
|
||||||
const out = sanitizeInlineHtml('safe <script>alert(1)</script> text')
|
const out = sanitizeInlineHtml('safe <script>alert(1)</script> text');
|
||||||
expect(out).not.toContain('<script')
|
expect(out).not.toContain('<script');
|
||||||
expect(out).not.toContain('alert(1)')
|
expect(out).not.toContain('alert(1)');
|
||||||
expect(out).toContain('safe')
|
expect(out).toContain('safe');
|
||||||
})
|
});
|
||||||
|
|
||||||
it('strips <img> (no img tag in inline allow-list)', () => {
|
it('strips <img> (no img tag in inline allow-list)', () => {
|
||||||
expect(sanitizeInlineHtml('<img src=x onerror=alert(1)>')).toBe('')
|
expect(sanitizeInlineHtml('<img src=x onerror=alert(1)>')).toBe('');
|
||||||
})
|
});
|
||||||
|
|
||||||
it('strips on* event handlers from preserved tags', () => {
|
it('strips on* event handlers from preserved tags', () => {
|
||||||
const out = sanitizeInlineHtml('<span onclick="alert(1)">hi</span>')
|
const out = sanitizeInlineHtml('<span onclick="alert(1)">hi</span>');
|
||||||
expect(out).not.toContain('onclick')
|
expect(out).not.toContain('onclick');
|
||||||
expect(out).toContain('hi')
|
expect(out).toContain('hi');
|
||||||
})
|
});
|
||||||
|
|
||||||
it('strips style attribute (CSS-injection surface)', () => {
|
it('strips style attribute (CSS-injection surface)', () => {
|
||||||
const out = sanitizeInlineHtml('<span style="background:url(javascript:alert(1))">x</span>')
|
const out = sanitizeInlineHtml(
|
||||||
expect(out).not.toContain('style=')
|
'<span style="background:url(javascript:alert(1))">x</span>',
|
||||||
expect(out).not.toContain('javascript:')
|
);
|
||||||
})
|
expect(out).not.toContain('style=');
|
||||||
|
expect(out).not.toContain('javascript:');
|
||||||
|
});
|
||||||
|
|
||||||
it('strips iframe / object / embed / svg-with-script', () => {
|
it('strips iframe / object / embed / svg-with-script', () => {
|
||||||
expect(sanitizeInlineHtml('<iframe src="evil"></iframe>')).toBe('')
|
expect(sanitizeInlineHtml('<iframe src="evil"></iframe>')).toBe('');
|
||||||
expect(sanitizeInlineHtml('<object data="evil"></object>')).toBe('')
|
expect(sanitizeInlineHtml('<object data="evil"></object>')).toBe('');
|
||||||
expect(sanitizeInlineHtml('<embed src="evil" />')).toBe('')
|
expect(sanitizeInlineHtml('<embed src="evil" />')).toBe('');
|
||||||
expect(sanitizeInlineHtml('<svg><script>alert(1)</script></svg>')).not.toContain('script')
|
expect(
|
||||||
})
|
sanitizeInlineHtml('<svg><script>alert(1)</script></svg>'),
|
||||||
|
).not.toContain('script');
|
||||||
|
});
|
||||||
|
|
||||||
it('does not preserve href / target on the inline tag set', () => {
|
it('does not preserve href / target on the inline tag set', () => {
|
||||||
// <a> is not in the inline allow-list, so href can never appear here.
|
// <a> is not in the inline allow-list, so href can never appear here.
|
||||||
const out = sanitizeInlineHtml('<a href="javascript:alert(1)">x</a>')
|
const out = sanitizeInlineHtml('<a href="javascript:alert(1)">x</a>');
|
||||||
expect(out).toBe('x')
|
expect(out).toBe('x');
|
||||||
})
|
});
|
||||||
|
|
||||||
it('keeps user text content when the wrapping tag is stripped', () => {
|
it('keeps user text content when the wrapping tag is stripped', () => {
|
||||||
expect(sanitizeInlineHtml('<custom-tag>hello</custom-tag>')).toBe('hello')
|
expect(sanitizeInlineHtml('<custom-tag>hello</custom-tag>')).toBe('hello');
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
describe('sanitizeRichTextHtml', () => {
|
describe('sanitizeRichTextHtml', () => {
|
||||||
it('preserves the full prose tag set', () => {
|
it('preserves the full prose tag set', () => {
|
||||||
const html = '<p>hello <strong>world</strong></p><ul><li>one</li></ul>'
|
const html = '<p>hello <strong>world</strong></p><ul><li>one</li></ul>';
|
||||||
const out = sanitizeRichTextHtml(html)
|
const out = sanitizeRichTextHtml(html);
|
||||||
expect(out).toContain('<p>')
|
expect(out).toContain('<p>');
|
||||||
expect(out).toContain('<strong>world</strong>')
|
expect(out).toContain('<strong>world</strong>');
|
||||||
expect(out).toContain('<li>one</li>')
|
expect(out).toContain('<li>one</li>');
|
||||||
})
|
});
|
||||||
|
|
||||||
it('still strips <script> + on* + style', () => {
|
it('still strips <script> + on* + style', () => {
|
||||||
const out = sanitizeRichTextHtml('<p onclick="alert(1)" style="x">hi</p><script>x()</script>')
|
const out = sanitizeRichTextHtml(
|
||||||
expect(out).not.toContain('onclick')
|
'<p onclick="alert(1)" style="x">hi</p><script>x()</script>',
|
||||||
expect(out).not.toContain('style=')
|
);
|
||||||
expect(out).not.toContain('<script')
|
expect(out).not.toContain('onclick');
|
||||||
})
|
expect(out).not.toContain('style=');
|
||||||
|
expect(out).not.toContain('<script');
|
||||||
|
});
|
||||||
|
|
||||||
it('blocks javascript: hrefs', () => {
|
it('blocks javascript: hrefs', () => {
|
||||||
const out = sanitizeRichTextHtml('<a href="javascript:alert(1)">x</a>')
|
const out = sanitizeRichTextHtml('<a href="javascript:alert(1)">x</a>');
|
||||||
expect(out).not.toContain('javascript:')
|
expect(out).not.toContain('javascript:');
|
||||||
})
|
});
|
||||||
|
|
||||||
it('blocks data: hrefs that smuggle scripts', () => {
|
it('blocks data: hrefs that smuggle scripts', () => {
|
||||||
const out = sanitizeRichTextHtml('<a href="data:text/html,<script>alert(1)</script>">x</a>')
|
const out = sanitizeRichTextHtml(
|
||||||
expect(out).not.toContain('data:text/html')
|
'<a href="data:text/html,<script>alert(1)</script>">x</a>',
|
||||||
})
|
);
|
||||||
|
expect(out).not.toContain('data:text/html');
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps http(s) hrefs intact', () => {
|
it('keeps http(s) hrefs intact', () => {
|
||||||
const out = sanitizeRichTextHtml('<a href="https://example.com">link</a>')
|
const out = sanitizeRichTextHtml('<a href="https://example.com">link</a>');
|
||||||
expect(out).toContain('href="https://example.com"')
|
expect(out).toContain('href="https://example.com"');
|
||||||
})
|
});
|
||||||
|
|
||||||
it('strips disallowed tags but keeps their content', () => {
|
it('strips disallowed tags but keeps their content', () => {
|
||||||
expect(sanitizeRichTextHtml('<p>before<custom>middle</custom>after</p>')).toContain('middle')
|
expect(
|
||||||
})
|
sanitizeRichTextHtml('<p>before<custom>middle</custom>after</p>'),
|
||||||
|
).toContain('middle');
|
||||||
|
});
|
||||||
|
|
||||||
it('drops mathml + svg shorthand vectors', () => {
|
it('drops mathml + svg shorthand vectors', () => {
|
||||||
const mathPayload = '<math><mtext><script>alert(1)</script></mtext></math>'
|
const mathPayload = '<math><mtext><script>alert(1)</script></mtext></math>';
|
||||||
const out = sanitizeRichTextHtml(mathPayload)
|
const out = sanitizeRichTextHtml(mathPayload);
|
||||||
expect(out).not.toContain('<script')
|
expect(out).not.toContain('<script');
|
||||||
expect(out).not.toContain('alert(1)')
|
expect(out).not.toContain('alert(1)');
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import DOMPurify from 'isomorphic-dompurify'
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTML sanitisation for TREK.
|
* HTML sanitisation for TREK.
|
||||||
@@ -18,18 +18,42 @@ import DOMPurify from 'isomorphic-dompurify'
|
|||||||
// expected in the surfaces we render today, so we keep the allow-list minimal
|
// expected in the surfaces we render today, so we keep the allow-list minimal
|
||||||
// and rely on `sanitizeRichTextHtml` when a richer surface needs full prose.
|
// and rely on `sanitizeRichTextHtml` when a richer surface needs full prose.
|
||||||
const INLINE_TAGS = [
|
const INLINE_TAGS = [
|
||||||
'b', 'strong', 'i', 'em', 'u', 's', 'del', 'ins',
|
'b',
|
||||||
'mark', 'code', 'sub', 'sup', 'br', 'span',
|
'strong',
|
||||||
] as const
|
'i',
|
||||||
|
'em',
|
||||||
|
'u',
|
||||||
|
's',
|
||||||
|
'del',
|
||||||
|
'ins',
|
||||||
|
'mark',
|
||||||
|
'code',
|
||||||
|
'sub',
|
||||||
|
'sup',
|
||||||
|
'br',
|
||||||
|
'span',
|
||||||
|
] as const;
|
||||||
|
|
||||||
const FULL_TAGS = [
|
const FULL_TAGS = [
|
||||||
...INLINE_TAGS,
|
...INLINE_TAGS,
|
||||||
'p', 'div', 'ul', 'ol', 'li',
|
'p',
|
||||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
'div',
|
||||||
'blockquote', 'pre', 'hr', 'a',
|
'ul',
|
||||||
] as const
|
'ol',
|
||||||
|
'li',
|
||||||
|
'h1',
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
'h4',
|
||||||
|
'h5',
|
||||||
|
'h6',
|
||||||
|
'blockquote',
|
||||||
|
'pre',
|
||||||
|
'hr',
|
||||||
|
'a',
|
||||||
|
] as const;
|
||||||
|
|
||||||
const SAFE_ATTRIBUTES = ['href', 'rel', 'target'] as const
|
const SAFE_ATTRIBUTES = ['href', 'rel', 'target'] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escapes the five HTML metacharacters so a raw string can be safely
|
* Escapes the five HTML metacharacters so a raw string can be safely
|
||||||
@@ -45,7 +69,7 @@ export function escapeHtml(value: string): string {
|
|||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
.replace(/'/g, ''')
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,13 +82,13 @@ export function escapeHtml(value: string): string {
|
|||||||
* built-in URL allow-list.
|
* built-in URL allow-list.
|
||||||
*/
|
*/
|
||||||
export function sanitizeInlineHtml(html: string): string {
|
export function sanitizeInlineHtml(html: string): string {
|
||||||
if (!html) return ''
|
if (!html) return '';
|
||||||
return DOMPurify.sanitize(html, {
|
return DOMPurify.sanitize(html, {
|
||||||
ALLOWED_TAGS: [...INLINE_TAGS],
|
ALLOWED_TAGS: [...INLINE_TAGS],
|
||||||
ALLOWED_ATTR: [],
|
ALLOWED_ATTR: [],
|
||||||
KEEP_CONTENT: true,
|
KEEP_CONTENT: true,
|
||||||
ALLOW_DATA_ATTR: false,
|
ALLOW_DATA_ATTR: false,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,10 +97,10 @@ export function sanitizeInlineHtml(html: string): string {
|
|||||||
* the inline sanitiser plus block-level markup and anchors with safe attrs.
|
* the inline sanitiser plus block-level markup and anchors with safe attrs.
|
||||||
*/
|
*/
|
||||||
export function sanitizeRichTextHtml(html: string): string {
|
export function sanitizeRichTextHtml(html: string): string {
|
||||||
if (!html) return ''
|
if (!html) return '';
|
||||||
return DOMPurify.sanitize(html, {
|
return DOMPurify.sanitize(html, {
|
||||||
ALLOWED_TAGS: [...FULL_TAGS],
|
ALLOWED_TAGS: [...FULL_TAGS],
|
||||||
ALLOWED_ATTR: [...SAFE_ATTRIBUTES],
|
ALLOWED_ATTR: [...SAFE_ATTRIBUTES],
|
||||||
ALLOW_DATA_ATTR: false,
|
ALLOW_DATA_ATTR: false,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,35 @@
|
|||||||
|
import {
|
||||||
|
settingUpsertRequestSchema,
|
||||||
|
settingsBulkRequestSchema,
|
||||||
|
MASKED_SETTING_VALUE,
|
||||||
|
} from './settings.schema';
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { settingUpsertRequestSchema, settingsBulkRequestSchema, MASKED_SETTING_VALUE } from './settings.schema';
|
|
||||||
|
|
||||||
describe('settingUpsertRequestSchema', () => {
|
describe('settingUpsertRequestSchema', () => {
|
||||||
it('requires a key; value is any/optional', () => {
|
it('requires a key; value is any/optional', () => {
|
||||||
expect(settingUpsertRequestSchema.safeParse({ key: 'theme', value: 'dark' }).success).toBe(true);
|
expect(
|
||||||
expect(settingUpsertRequestSchema.safeParse({ key: 'theme' }).success).toBe(true);
|
settingUpsertRequestSchema.safeParse({ key: 'theme', value: 'dark' })
|
||||||
expect(settingUpsertRequestSchema.safeParse({ value: 'dark' }).success).toBe(false);
|
.success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(settingUpsertRequestSchema.safeParse({ key: 'theme' }).success).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
settingUpsertRequestSchema.safeParse({ value: 'dark' }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('settingsBulkRequestSchema', () => {
|
describe('settingsBulkRequestSchema', () => {
|
||||||
it('requires a settings record', () => {
|
it('requires a settings record', () => {
|
||||||
expect(settingsBulkRequestSchema.safeParse({ settings: { a: 1, b: 'x' } }).success).toBe(true);
|
expect(
|
||||||
expect(settingsBulkRequestSchema.safeParse({ settings: {} }).success).toBe(true);
|
settingsBulkRequestSchema.safeParse({ settings: { a: 1, b: 'x' } })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(settingsBulkRequestSchema.safeParse({ settings: {} }).success).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
expect(settingsBulkRequestSchema.safeParse({}).success).toBe(false);
|
expect(settingsBulkRequestSchema.safeParse({}).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { shareLinkRequestSchema } from './share.schema';
|
import { shareLinkRequestSchema } from './share.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('shareLinkRequestSchema', () => {
|
describe('shareLinkRequestSchema', () => {
|
||||||
it('accepts any subset of section toggles (all optional) and an empty body', () => {
|
it('accepts any subset of section toggles (all optional) and an empty body', () => {
|
||||||
expect(shareLinkRequestSchema.safeParse({ share_map: true, share_budget: false }).success).toBe(true);
|
expect(
|
||||||
|
shareLinkRequestSchema.safeParse({ share_map: true, share_budget: false })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
expect(shareLinkRequestSchema.safeParse({}).success).toBe(true);
|
expect(shareLinkRequestSchema.safeParse({}).success).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects a non-boolean toggle', () => {
|
it('rejects a non-boolean toggle', () => {
|
||||||
expect(shareLinkRequestSchema.safeParse({ share_map: 'yes' }).success).toBe(false);
|
expect(shareLinkRequestSchema.safeParse({ share_map: 'yes' }).success).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,42 +1,78 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { systemNoticeDtoSchema } from './system-notice.schema';
|
import { systemNoticeDtoSchema } from './system-notice.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('systemNoticeDtoSchema', () => {
|
describe('systemNoticeDtoSchema', () => {
|
||||||
it('accepts a minimal notice (required fields only)', () => {
|
it('accepts a minimal notice (required fields only)', () => {
|
||||||
const parsed = systemNoticeDtoSchema.parse({
|
const parsed = systemNoticeDtoSchema.parse({
|
||||||
id: 'welcome', display: 'modal', severity: 'info',
|
id: 'welcome',
|
||||||
titleKey: 'notice.welcome.title', bodyKey: 'notice.welcome.body', dismissible: true,
|
display: 'modal',
|
||||||
|
severity: 'info',
|
||||||
|
titleKey: 'notice.welcome.title',
|
||||||
|
bodyKey: 'notice.welcome.body',
|
||||||
|
dismissible: true,
|
||||||
});
|
});
|
||||||
expect(parsed.id).toBe('welcome');
|
expect(parsed.id).toBe('welcome');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts a rich notice with media, highlights and a nav CTA', () => {
|
it('accepts a rich notice with media, highlights and a nav CTA', () => {
|
||||||
expect(systemNoticeDtoSchema.safeParse({
|
expect(
|
||||||
id: 'release', display: 'banner', severity: 'warn',
|
systemNoticeDtoSchema.safeParse({
|
||||||
titleKey: 't', bodyKey: 'b', dismissible: false,
|
id: 'release',
|
||||||
bodyParams: { version: '3.1' },
|
display: 'banner',
|
||||||
icon: 'sparkles',
|
severity: 'warn',
|
||||||
media: { src: '/img.png', altKey: 'alt', placement: 'hero' },
|
titleKey: 't',
|
||||||
highlights: [{ labelKey: 'h1', iconName: 'check' }],
|
bodyKey: 'b',
|
||||||
cta: { kind: 'nav', labelKey: 'open', href: '/whats-new' },
|
dismissible: false,
|
||||||
}).success).toBe(true);
|
bodyParams: { version: '3.1' },
|
||||||
|
icon: 'sparkles',
|
||||||
|
media: { src: '/img.png', altKey: 'alt', placement: 'hero' },
|
||||||
|
highlights: [{ labelKey: 'h1', iconName: 'check' }],
|
||||||
|
cta: { kind: 'nav', labelKey: 'open', href: '/whats-new' },
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts an action CTA with the discriminated-union shape', () => {
|
it('accepts an action CTA with the discriminated-union shape', () => {
|
||||||
expect(systemNoticeDtoSchema.safeParse({
|
expect(
|
||||||
id: 'x', display: 'toast', severity: 'critical',
|
systemNoticeDtoSchema.safeParse({
|
||||||
titleKey: 't', bodyKey: 'b', dismissible: true,
|
id: 'x',
|
||||||
cta: { kind: 'action', labelKey: 'do', actionId: 'reload', dismissOnAction: true },
|
display: 'toast',
|
||||||
}).success).toBe(true);
|
severity: 'critical',
|
||||||
|
titleKey: 't',
|
||||||
|
bodyKey: 'b',
|
||||||
|
dismissible: true,
|
||||||
|
cta: {
|
||||||
|
kind: 'action',
|
||||||
|
labelKey: 'do',
|
||||||
|
actionId: 'reload',
|
||||||
|
dismissOnAction: true,
|
||||||
|
},
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects an unknown display value and a malformed CTA', () => {
|
it('rejects an unknown display value and a malformed CTA', () => {
|
||||||
expect(systemNoticeDtoSchema.safeParse({
|
expect(
|
||||||
id: 'x', display: 'popup', severity: 'info', titleKey: 't', bodyKey: 'b', dismissible: true,
|
systemNoticeDtoSchema.safeParse({
|
||||||
}).success).toBe(false);
|
id: 'x',
|
||||||
expect(systemNoticeDtoSchema.safeParse({
|
display: 'popup',
|
||||||
id: 'x', display: 'modal', severity: 'info', titleKey: 't', bodyKey: 'b', dismissible: true,
|
severity: 'info',
|
||||||
cta: { kind: 'nav', labelKey: 'open' },
|
titleKey: 't',
|
||||||
}).success).toBe(false);
|
bodyKey: 'b',
|
||||||
|
dismissible: true,
|
||||||
|
}).success,
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
systemNoticeDtoSchema.safeParse({
|
||||||
|
id: 'x',
|
||||||
|
display: 'modal',
|
||||||
|
severity: 'info',
|
||||||
|
titleKey: 't',
|
||||||
|
bodyKey: 'b',
|
||||||
|
dismissible: true,
|
||||||
|
cta: { kind: 'nav', labelKey: 'open' },
|
||||||
|
}).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,29 @@
|
|||||||
|
import {
|
||||||
|
tagSchema,
|
||||||
|
createTagRequestSchema,
|
||||||
|
updateTagRequestSchema,
|
||||||
|
} from './tag.schema';
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { tagSchema, createTagRequestSchema, updateTagRequestSchema } from './tag.schema';
|
|
||||||
|
|
||||||
describe('tagSchema', () => {
|
describe('tagSchema', () => {
|
||||||
it('accepts a full tag', () => {
|
it('accepts a full tag', () => {
|
||||||
expect(tagSchema.safeParse({ id: 1, user_id: 5, name: 'Beach', color: '#10b981' }).success).toBe(true);
|
expect(
|
||||||
|
tagSchema.safeParse({
|
||||||
|
id: 1,
|
||||||
|
user_id: 5,
|
||||||
|
name: 'Beach',
|
||||||
|
color: '#10b981',
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createTagRequestSchema', () => {
|
describe('createTagRequestSchema', () => {
|
||||||
it('requires a non-empty name; colour optional', () => {
|
it('requires a non-empty name; colour optional', () => {
|
||||||
expect(createTagRequestSchema.safeParse({ name: 'Beach' }).success).toBe(true);
|
expect(createTagRequestSchema.safeParse({ name: 'Beach' }).success).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
expect(createTagRequestSchema.safeParse({ name: '' }).success).toBe(false);
|
expect(createTagRequestSchema.safeParse({ name: '' }).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,30 +1,51 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
import {
|
||||||
todoCreateItemRequestSchema,
|
todoCreateItemRequestSchema,
|
||||||
todoUpdateItemRequestSchema,
|
todoUpdateItemRequestSchema,
|
||||||
todoReorderRequestSchema,
|
todoReorderRequestSchema,
|
||||||
} from './todo.schema';
|
} from './todo.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('todoCreateItemRequestSchema', () => {
|
describe('todoCreateItemRequestSchema', () => {
|
||||||
it('requires a name; metadata optional with the service shapes', () => {
|
it('requires a name; metadata optional with the service shapes', () => {
|
||||||
expect(todoCreateItemRequestSchema.safeParse({ name: 'Book hotel' }).success).toBe(true);
|
expect(
|
||||||
expect(todoCreateItemRequestSchema.safeParse({ name: 'X', due_date: '2026-07-01', priority: 2, assigned_user_id: 3 }).success).toBe(true);
|
todoCreateItemRequestSchema.safeParse({ name: 'Book hotel' }).success,
|
||||||
expect(todoCreateItemRequestSchema.safeParse({ name: '' }).success).toBe(false);
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
todoCreateItemRequestSchema.safeParse({
|
||||||
|
name: 'X',
|
||||||
|
due_date: '2026-07-01',
|
||||||
|
priority: 2,
|
||||||
|
assigned_user_id: 3,
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(todoCreateItemRequestSchema.safeParse({ name: '' }).success).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
// priority is numeric (matches the service), not a string
|
// priority is numeric (matches the service), not a string
|
||||||
expect(todoCreateItemRequestSchema.safeParse({ name: 'X', priority: 'high' }).success).toBe(false);
|
expect(
|
||||||
|
todoCreateItemRequestSchema.safeParse({ name: 'X', priority: 'high' })
|
||||||
|
.success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('todoUpdateItemRequestSchema', () => {
|
describe('todoUpdateItemRequestSchema', () => {
|
||||||
it('allows every field to be omitted and accepts checked', () => {
|
it('allows every field to be omitted and accepts checked', () => {
|
||||||
expect(todoUpdateItemRequestSchema.safeParse({}).success).toBe(true);
|
expect(todoUpdateItemRequestSchema.safeParse({}).success).toBe(true);
|
||||||
expect(todoUpdateItemRequestSchema.safeParse({ checked: true }).success).toBe(true);
|
expect(
|
||||||
|
todoUpdateItemRequestSchema.safeParse({ checked: true }).success,
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('todoReorderRequestSchema', () => {
|
describe('todoReorderRequestSchema', () => {
|
||||||
it('requires an array of numeric ids', () => {
|
it('requires an array of numeric ids', () => {
|
||||||
expect(todoReorderRequestSchema.safeParse({ orderedIds: [1, 2, 3] }).success).toBe(true);
|
expect(
|
||||||
expect(todoReorderRequestSchema.safeParse({ orderedIds: ['a'] }).success).toBe(false);
|
todoReorderRequestSchema.safeParse({ orderedIds: [1, 2, 3] }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
todoReorderRequestSchema.safeParse({ orderedIds: ['a'] }).success,
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,4 +39,6 @@ export type TodoReorderRequest = z.infer<typeof todoReorderRequestSchema>;
|
|||||||
export const todoCategoryAssigneesRequestSchema = z.object({
|
export const todoCategoryAssigneesRequestSchema = z.object({
|
||||||
user_ids: z.array(z.number()),
|
user_ids: z.array(z.number()),
|
||||||
});
|
});
|
||||||
export type TodoCategoryAssigneesRequest = z.infer<typeof todoCategoryAssigneesRequestSchema>;
|
export type TodoCategoryAssigneesRequest = z.infer<
|
||||||
|
typeof todoCategoryAssigneesRequestSchema
|
||||||
|
>;
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
import {
|
||||||
tripCreateRequestSchema,
|
tripCreateRequestSchema,
|
||||||
tripUpdateRequestSchema,
|
tripUpdateRequestSchema,
|
||||||
tripAddMemberRequestSchema,
|
tripAddMemberRequestSchema,
|
||||||
} from './trip.schema';
|
} from './trip.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('tripCreateRequestSchema', () => {
|
describe('tripCreateRequestSchema', () => {
|
||||||
it('requires a title; dates/currency/reminder optional', () => {
|
it('requires a title; dates/currency/reminder optional', () => {
|
||||||
expect(tripCreateRequestSchema.safeParse({ title: 'Japan' }).success).toBe(true);
|
expect(tripCreateRequestSchema.safeParse({ title: 'Japan' }).success).toBe(
|
||||||
expect(tripCreateRequestSchema.safeParse({ title: 'Japan', start_date: '2026-07-01', day_count: 7 }).success).toBe(true);
|
true,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
tripCreateRequestSchema.safeParse({
|
||||||
|
title: 'Japan',
|
||||||
|
start_date: '2026-07-01',
|
||||||
|
day_count: 7,
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
expect(tripCreateRequestSchema.safeParse({}).success).toBe(false);
|
expect(tripCreateRequestSchema.safeParse({}).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -16,13 +25,18 @@ describe('tripCreateRequestSchema', () => {
|
|||||||
describe('tripUpdateRequestSchema', () => {
|
describe('tripUpdateRequestSchema', () => {
|
||||||
it('is fully partial and accepts is_archived + cover_image', () => {
|
it('is fully partial and accepts is_archived + cover_image', () => {
|
||||||
expect(tripUpdateRequestSchema.safeParse({}).success).toBe(true);
|
expect(tripUpdateRequestSchema.safeParse({}).success).toBe(true);
|
||||||
expect(tripUpdateRequestSchema.safeParse({ is_archived: 1, cover_image: null }).success).toBe(true);
|
expect(
|
||||||
|
tripUpdateRequestSchema.safeParse({ is_archived: 1, cover_image: null })
|
||||||
|
.success,
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('tripAddMemberRequestSchema', () => {
|
describe('tripAddMemberRequestSchema', () => {
|
||||||
it('requires an identifier', () => {
|
it('requires an identifier', () => {
|
||||||
expect(tripAddMemberRequestSchema.safeParse({ identifier: 'bob@x.y' }).success).toBe(true);
|
expect(
|
||||||
|
tripAddMemberRequestSchema.safeParse({ identifier: 'bob@x.y' }).success,
|
||||||
|
).toBe(true);
|
||||||
expect(tripAddMemberRequestSchema.safeParse({}).success).toBe(false);
|
expect(tripAddMemberRequestSchema.safeParse({}).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
import {
|
||||||
vacayAddHolidayCalendarRequestSchema,
|
vacayAddHolidayCalendarRequestSchema,
|
||||||
vacayInviteRequestSchema,
|
vacayInviteRequestSchema,
|
||||||
@@ -6,34 +5,61 @@ import {
|
|||||||
vacayAddYearRequestSchema,
|
vacayAddYearRequestSchema,
|
||||||
} from './vacay.schema';
|
} from './vacay.schema';
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('vacayAddHolidayCalendarRequestSchema', () => {
|
describe('vacayAddHolidayCalendarRequestSchema', () => {
|
||||||
it('requires a region; label/color/sort_order optional', () => {
|
it('requires a region; label/color/sort_order optional', () => {
|
||||||
expect(vacayAddHolidayCalendarRequestSchema.safeParse({ region: 'DE-BY' }).success).toBe(true);
|
expect(
|
||||||
expect(vacayAddHolidayCalendarRequestSchema.safeParse({ region: 'DE-BY', label: null }).success).toBe(true);
|
vacayAddHolidayCalendarRequestSchema.safeParse({ region: 'DE-BY' })
|
||||||
expect(vacayAddHolidayCalendarRequestSchema.safeParse({}).success).toBe(false);
|
.success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
vacayAddHolidayCalendarRequestSchema.safeParse({
|
||||||
|
region: 'DE-BY',
|
||||||
|
label: null,
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(vacayAddHolidayCalendarRequestSchema.safeParse({}).success).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('vacayInviteRequestSchema', () => {
|
describe('vacayInviteRequestSchema', () => {
|
||||||
it('accepts a numeric or string user_id', () => {
|
it('accepts a numeric or string user_id', () => {
|
||||||
expect(vacayInviteRequestSchema.safeParse({ user_id: 2 }).success).toBe(true);
|
expect(vacayInviteRequestSchema.safeParse({ user_id: 2 }).success).toBe(
|
||||||
expect(vacayInviteRequestSchema.safeParse({ user_id: '2' }).success).toBe(true);
|
true,
|
||||||
|
);
|
||||||
|
expect(vacayInviteRequestSchema.safeParse({ user_id: '2' }).success).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
expect(vacayInviteRequestSchema.safeParse({}).success).toBe(false);
|
expect(vacayInviteRequestSchema.safeParse({}).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('vacayToggleEntryRequestSchema', () => {
|
describe('vacayToggleEntryRequestSchema', () => {
|
||||||
it('requires a date; target_user_id optional', () => {
|
it('requires a date; target_user_id optional', () => {
|
||||||
expect(vacayToggleEntryRequestSchema.safeParse({ date: '2026-07-01' }).success).toBe(true);
|
expect(
|
||||||
expect(vacayToggleEntryRequestSchema.safeParse({ date: '2026-07-01', target_user_id: 3 }).success).toBe(true);
|
vacayToggleEntryRequestSchema.safeParse({ date: '2026-07-01' }).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
vacayToggleEntryRequestSchema.safeParse({
|
||||||
|
date: '2026-07-01',
|
||||||
|
target_user_id: 3,
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
expect(vacayToggleEntryRequestSchema.safeParse({}).success).toBe(false);
|
expect(vacayToggleEntryRequestSchema.safeParse({}).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('vacayAddYearRequestSchema', () => {
|
describe('vacayAddYearRequestSchema', () => {
|
||||||
it('accepts a numeric or string year', () => {
|
it('accepts a numeric or string year', () => {
|
||||||
expect(vacayAddYearRequestSchema.safeParse({ year: 2027 }).success).toBe(true);
|
expect(vacayAddYearRequestSchema.safeParse({ year: 2027 }).success).toBe(
|
||||||
expect(vacayAddYearRequestSchema.safeParse({ year: '2027' }).success).toBe(true);
|
true,
|
||||||
|
);
|
||||||
|
expect(vacayAddYearRequestSchema.safeParse({ year: '2027' }).success).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
expect(vacayAddYearRequestSchema.safeParse({}).success).toBe(false);
|
expect(vacayAddYearRequestSchema.safeParse({}).success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ export const vacayAddHolidayCalendarRequestSchema = z.object({
|
|||||||
color: z.string().optional(),
|
color: z.string().optional(),
|
||||||
sort_order: z.number().optional(),
|
sort_order: z.number().optional(),
|
||||||
});
|
});
|
||||||
export type VacayAddHolidayCalendarRequest = z.infer<typeof vacayAddHolidayCalendarRequestSchema>;
|
export type VacayAddHolidayCalendarRequest = z.infer<
|
||||||
|
typeof vacayAddHolidayCalendarRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const vacaySetColorRequestSchema = z.object({
|
export const vacaySetColorRequestSchema = z.object({
|
||||||
color: z.string().optional(),
|
color: z.string().optional(),
|
||||||
@@ -38,7 +40,9 @@ export type VacayInviteRequest = z.infer<typeof vacayInviteRequestSchema>;
|
|||||||
export const vacayInviteActionRequestSchema = z.object({
|
export const vacayInviteActionRequestSchema = z.object({
|
||||||
plan_id: z.number().optional(),
|
plan_id: z.number().optional(),
|
||||||
});
|
});
|
||||||
export type VacayInviteActionRequest = z.infer<typeof vacayInviteActionRequestSchema>;
|
export type VacayInviteActionRequest = z.infer<
|
||||||
|
typeof vacayInviteActionRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const vacayAddYearRequestSchema = z.object({
|
export const vacayAddYearRequestSchema = z.object({
|
||||||
year: z.union([z.number(), z.string()]),
|
year: z.union([z.number(), z.string()]),
|
||||||
@@ -49,19 +53,25 @@ export const vacayToggleEntryRequestSchema = z.object({
|
|||||||
date: z.string().min(1),
|
date: z.string().min(1),
|
||||||
target_user_id: z.union([z.number(), z.string()]).optional(),
|
target_user_id: z.union([z.number(), z.string()]).optional(),
|
||||||
});
|
});
|
||||||
export type VacayToggleEntryRequest = z.infer<typeof vacayToggleEntryRequestSchema>;
|
export type VacayToggleEntryRequest = z.infer<
|
||||||
|
typeof vacayToggleEntryRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const vacayCompanyHolidayRequestSchema = z.object({
|
export const vacayCompanyHolidayRequestSchema = z.object({
|
||||||
date: z.string(),
|
date: z.string(),
|
||||||
note: z.string().optional(),
|
note: z.string().optional(),
|
||||||
});
|
});
|
||||||
export type VacayCompanyHolidayRequest = z.infer<typeof vacayCompanyHolidayRequestSchema>;
|
export type VacayCompanyHolidayRequest = z.infer<
|
||||||
|
typeof vacayCompanyHolidayRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const vacayUpdateStatsRequestSchema = z.object({
|
export const vacayUpdateStatsRequestSchema = z.object({
|
||||||
vacation_days: z.number().optional(),
|
vacation_days: z.number().optional(),
|
||||||
target_user_id: z.union([z.number(), z.string()]).optional(),
|
target_user_id: z.union([z.number(), z.string()]).optional(),
|
||||||
});
|
});
|
||||||
export type VacayUpdateStatsRequest = z.infer<typeof vacayUpdateStatsRequestSchema>;
|
export type VacayUpdateStatsRequest = z.infer<
|
||||||
|
typeof vacayUpdateStatsRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
/** Plan / entries / stats payloads are wide and DB-derived; kept open. */
|
/** Plan / entries / stats payloads are wide and DB-derived; kept open. */
|
||||||
export const vacayPlanDataSchema = open;
|
export const vacayPlanDataSchema = open;
|
||||||
|
|||||||
Reference in New Issue
Block a user