mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +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 { adminUserCreateRequestSchema, adminPermissionsRequestSchema, adminInviteCreateRequestSchema, adminFeatureToggleRequestSchema } from './admin.schema';
|
||||
|
||||
describe('adminUserCreateRequestSchema', () => {
|
||||
it('requires an email; role limited to user/admin', () => {
|
||||
expect(adminUserCreateRequestSchema.safeParse({ email: 'a@b.c', 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);
|
||||
expect(
|
||||
adminUserCreateRequestSchema.safeParse({
|
||||
email: 'a@b.c',
|
||||
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', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('adminInviteCreateRequestSchema', () => {
|
||||
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({ role: 'root' }).success).toBe(false);
|
||||
expect(
|
||||
adminInviteCreateRequestSchema.safeParse({ role: 'root' }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('adminFeatureToggleRequestSchema', () => {
|
||||
it('requires a boolean enabled', () => {
|
||||
expect(adminFeatureToggleRequestSchema.safeParse({ enabled: true }).success).toBe(true);
|
||||
expect(adminFeatureToggleRequestSchema.safeParse({ enabled: 'yes' }).success).toBe(false);
|
||||
expect(
|
||||
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(),
|
||||
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({
|
||||
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({
|
||||
max_uses: z.number().optional(),
|
||||
expires_in_days: z.number().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({
|
||||
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 { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('airportSchema', () => {
|
||||
it('accepts a full airport record', () => {
|
||||
const parsed = airportSchema.parse({
|
||||
iata: 'BER', icao: 'EDDB', name: 'Berlin Brandenburg', city: 'Berlin',
|
||||
country: 'DE', lat: 52.36, lng: 13.5, tz: 'Europe/Berlin',
|
||||
iata: 'BER',
|
||||
icao: 'EDDB',
|
||||
name: 'Berlin Brandenburg',
|
||||
city: 'Berlin',
|
||||
country: 'DE',
|
||||
lat: 52.36,
|
||||
lng: 13.5,
|
||||
tz: 'Europe/Berlin',
|
||||
});
|
||||
expect(parsed.iata).toBe('BER');
|
||||
});
|
||||
|
||||
it('allows a null icao (smaller fields can be missing one)', () => {
|
||||
expect(airportSchema.safeParse({
|
||||
iata: 'XXX', icao: null, name: 'Test', city: 'Test', country: 'DE',
|
||||
lat: 0, lng: 0, tz: 'UTC',
|
||||
}).success).toBe(true);
|
||||
expect(
|
||||
airportSchema.safeParse({
|
||||
iata: 'XXX',
|
||||
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 {
|
||||
assignmentCreateRequestSchema,
|
||||
assignmentMoveRequestSchema,
|
||||
assignmentParticipantsRequestSchema,
|
||||
} from './assignment.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('assignmentCreateRequestSchema', () => {
|
||||
it('requires a place_id; notes optional/nullable', () => {
|
||||
expect(assignmentCreateRequestSchema.safeParse({ place_id: 2 }).success).toBe(true);
|
||||
expect(assignmentCreateRequestSchema.safeParse({ place_id: '2', notes: null }).success).toBe(true);
|
||||
expect(
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('assignmentMoveRequestSchema', () => {
|
||||
it('requires new_day_id; order_index optional', () => {
|
||||
expect(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({ 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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('assignmentParticipantsRequestSchema', () => {
|
||||
it('requires a numeric user_ids array', () => {
|
||||
expect(assignmentParticipantsRequestSchema.safeParse({ user_ids: [1, 2] }).success).toBe(true);
|
||||
expect(assignmentParticipantsRequestSchema.safeParse({ user_ids: 'no' }).success).toBe(false);
|
||||
expect(
|
||||
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 { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Assignment API contract — single source of truth for the place↔day itinerary
|
||||
* 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()]),
|
||||
notes: z.string().nullable().optional(),
|
||||
});
|
||||
export type AssignmentCreateRequest = z.infer<typeof assignmentCreateRequestSchema>;
|
||||
export type AssignmentCreateRequest = z.infer<
|
||||
typeof assignmentCreateRequestSchema
|
||||
>;
|
||||
|
||||
export const assignmentReorderRequestSchema = z.object({
|
||||
orderedIds: z.array(z.number()),
|
||||
});
|
||||
export type AssignmentReorderRequest = z.infer<typeof assignmentReorderRequestSchema>;
|
||||
export type AssignmentReorderRequest = z.infer<
|
||||
typeof assignmentReorderRequestSchema
|
||||
>;
|
||||
|
||||
export const assignmentMoveRequestSchema = z.object({
|
||||
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({
|
||||
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 {
|
||||
markRegionRequestSchema,
|
||||
createBucketItemRequestSchema,
|
||||
regionGeoSchema,
|
||||
} from './atlas.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('markRegionRequestSchema', () => {
|
||||
it('requires both name and country_code', () => {
|
||||
expect(markRegionRequestSchema.safeParse({ name: 'Bavaria', country_code: 'DE' }).success).toBe(true);
|
||||
expect(markRegionRequestSchema.safeParse({ name: 'Bavaria' }).success).toBe(false);
|
||||
expect(
|
||||
markRegionRequestSchema.safeParse({ name: 'Bavaria', country_code: 'DE' })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(markRegionRequestSchema.safeParse({ name: 'Bavaria' }).success).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBucketItemRequestSchema', () => {
|
||||
it('requires a name; coordinates and metadata optional/nullable', () => {
|
||||
expect(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({ 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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('regionGeoSchema', () => {
|
||||
it('accepts a FeatureCollection with opaque features', () => {
|
||||
expect(regionGeoSchema.safeParse({ type: 'FeatureCollection', features: [] }).success).toBe(true);
|
||||
expect(regionGeoSchema.safeParse({ type: 'FeatureCollection', features: [{ anything: true }] }).success).toBe(true);
|
||||
expect(regionGeoSchema.safeParse({ type: 'Other', features: [] }).success).toBe(false);
|
||||
expect(
|
||||
regionGeoSchema.safeParse({ type: 'FeatureCollection', features: [] })
|
||||
.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(),
|
||||
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({
|
||||
name: z.string().optional(),
|
||||
@@ -39,7 +41,9 @@ export const updateBucketItemRequestSchema = z.object({
|
||||
country_code: 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). */
|
||||
export const bucketItemSchema = open;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
registerRequestSchema,
|
||||
loginRequestSchema,
|
||||
@@ -10,38 +9,84 @@ import {
|
||||
mcpTokenCreateRequestSchema,
|
||||
} from './auth.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('registerRequestSchema', () => {
|
||||
it('requires email + password; username/invite optional', () => {
|
||||
expect(registerRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' }).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);
|
||||
expect(
|
||||
registerRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' })
|
||||
.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', () => {
|
||||
it('requires email + password', () => {
|
||||
expect(loginRequestSchema.safeParse({ email: 'a@b.c', password: 'pw' }).success).toBe(true);
|
||||
expect(loginRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(false);
|
||||
expect(
|
||||
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', () => {
|
||||
it('validate their required fields', () => {
|
||||
expect(forgotPasswordRequestSchema.safeParse({ email: 'a@b.c' }).success).toBe(true);
|
||||
expect(resetPasswordRequestSchema.safeParse({ token: 't', new_password: 'pw' }).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);
|
||||
expect(
|
||||
forgotPasswordRequestSchema.safeParse({ email: 'a@b.c' }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
resetPasswordRequestSchema.safeParse({ token: 't', new_password: 'pw' })
|
||||
.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', () => {
|
||||
it('validate their fields', () => {
|
||||
expect(mfaVerifyLoginRequestSchema.safeParse({ mfa_token: 't', code: '123456' }).success).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(
|
||||
mfaVerifyLoginRequestSchema.safeParse({ mfa_token: 't', code: '123456' })
|
||||
.success,
|
||||
).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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { autoBackupSettingsRequestSchema } from './backup.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('autoBackupSettingsRequestSchema', () => {
|
||||
it('accepts the known toggles and stays permissive for extras', () => {
|
||||
expect(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({
|
||||
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);
|
||||
});
|
||||
|
||||
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(),
|
||||
})
|
||||
.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 {
|
||||
budgetCreateItemRequestSchema,
|
||||
budgetUpdateMembersRequestSchema,
|
||||
@@ -6,31 +5,54 @@ import {
|
||||
budgetReorderItemsRequestSchema,
|
||||
} from './budget.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('budgetCreateItemRequestSchema', () => {
|
||||
it('requires a name; money/meta fields optional + nullable', () => {
|
||||
expect(budgetCreateItemRequestSchema.safeParse({ name: 'Hotel' }).success).toBe(true);
|
||||
expect(budgetCreateItemRequestSchema.safeParse({ name: 'Hotel', total_price: 200, persons: null }).success).toBe(true);
|
||||
expect(
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('budgetUpdateMembersRequestSchema', () => {
|
||||
it('requires a numeric user_ids array', () => {
|
||||
expect(budgetUpdateMembersRequestSchema.safeParse({ user_ids: [1, 2] }).success).toBe(true);
|
||||
expect(budgetUpdateMembersRequestSchema.safeParse({ user_ids: 'no' }).success).toBe(false);
|
||||
expect(
|
||||
budgetUpdateMembersRequestSchema.safeParse({ user_ids: [1, 2] }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
budgetUpdateMembersRequestSchema.safeParse({ user_ids: 'no' }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('budgetToggleMemberPaidRequestSchema', () => {
|
||||
it('requires a boolean paid', () => {
|
||||
expect(budgetToggleMemberPaidRequestSchema.safeParse({ paid: true }).success).toBe(true);
|
||||
expect(budgetToggleMemberPaidRequestSchema.safeParse({ paid: 'yes' }).success).toBe(false);
|
||||
expect(
|
||||
budgetToggleMemberPaidRequestSchema.safeParse({ paid: true }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
budgetToggleMemberPaidRequestSchema.safeParse({ paid: 'yes' }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('budgetReorderItemsRequestSchema', () => {
|
||||
it('requires numeric ids', () => {
|
||||
expect(budgetReorderItemsRequestSchema.safeParse({ orderedIds: [3, 1, 2] }).success).toBe(true);
|
||||
expect(budgetReorderItemsRequestSchema.safeParse({ orderedIds: ['a'] }).success).toBe(false);
|
||||
expect(
|
||||
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(),
|
||||
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. */
|
||||
export const budgetUpdateItemRequestSchema = z.object({
|
||||
@@ -71,24 +73,34 @@ export const budgetUpdateItemRequestSchema = z.object({
|
||||
note: 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({
|
||||
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({
|
||||
paid: z.boolean(),
|
||||
});
|
||||
export type BudgetToggleMemberPaidRequest = z.infer<typeof budgetToggleMemberPaidRequestSchema>;
|
||||
export type BudgetToggleMemberPaidRequest = z.infer<
|
||||
typeof budgetToggleMemberPaidRequestSchema
|
||||
>;
|
||||
|
||||
export const budgetReorderItemsRequestSchema = z.object({
|
||||
orderedIds: z.array(z.number()),
|
||||
});
|
||||
export type BudgetReorderItemsRequest = z.infer<typeof budgetReorderItemsRequestSchema>;
|
||||
export type BudgetReorderItemsRequest = z.infer<
|
||||
typeof budgetReorderItemsRequestSchema
|
||||
>;
|
||||
|
||||
export const budgetReorderCategoriesRequestSchema = z.object({
|
||||
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 {
|
||||
categorySchema,
|
||||
createCategoryRequestSchema,
|
||||
updateCategoryRequestSchema,
|
||||
} from './category.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('categorySchema', () => {
|
||||
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', () => {
|
||||
it('requires a non-empty name; colour and icon are optional', () => {
|
||||
expect(createCategoryRequestSchema.safeParse({ name: 'Food' }).success).toBe(true);
|
||||
expect(createCategoryRequestSchema.safeParse({ name: '' }).success).toBe(false);
|
||||
expect(
|
||||
createCategoryRequestSchema.safeParse({ name: 'Food' }).success,
|
||||
).toBe(true);
|
||||
expect(createCategoryRequestSchema.safeParse({ name: '' }).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(createCategoryRequestSchema.safeParse({}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -22,6 +34,8 @@ describe('createCategoryRequestSchema', () => {
|
||||
describe('updateCategoryRequestSchema', () => {
|
||||
it('allows every field to be omitted (the service COALESCEs)', () => {
|
||||
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 {
|
||||
collabNoteCreateRequestSchema,
|
||||
collabPollCreateRequestSchema,
|
||||
@@ -7,41 +6,78 @@ import {
|
||||
collabReactionRequestSchema,
|
||||
} from './collab.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('collabNoteCreateRequestSchema', () => {
|
||||
it('requires a non-empty title; the rest is optional', () => {
|
||||
expect(collabNoteCreateRequestSchema.safeParse({ title: 'Idea' }).success).toBe(true);
|
||||
expect(collabNoteCreateRequestSchema.safeParse({ title: '' }).success).toBe(false);
|
||||
expect(
|
||||
collabNoteCreateRequestSchema.safeParse({ title: 'Idea' }).success,
|
||||
).toBe(true);
|
||||
expect(collabNoteCreateRequestSchema.safeParse({ title: '' }).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(collabNoteCreateRequestSchema.safeParse({}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collabPollCreateRequestSchema', () => {
|
||||
it('requires a question and at least two options', () => {
|
||||
expect(collabPollCreateRequestSchema.safeParse({ 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);
|
||||
expect(
|
||||
collabPollCreateRequestSchema.safeParse({
|
||||
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', () => {
|
||||
it('requires a numeric option_index', () => {
|
||||
expect(collabPollVoteRequestSchema.safeParse({ option_index: 0 }).success).toBe(true);
|
||||
expect(collabPollVoteRequestSchema.safeParse({ option_index: 'a' }).success).toBe(false);
|
||||
expect(
|
||||
collabPollVoteRequestSchema.safeParse({ option_index: 0 }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
collabPollVoteRequestSchema.safeParse({ option_index: 'a' }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collabMessageCreateRequestSchema', () => {
|
||||
it('requires text, caps it at 5000, allows a nullable reply_to', () => {
|
||||
expect(collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: null }).success).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);
|
||||
expect(
|
||||
collabMessageCreateRequestSchema.safeParse({ text: 'hi', reply_to: null })
|
||||
.success,
|
||||
).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', () => {
|
||||
it('requires a non-empty emoji', () => {
|
||||
expect(collabReactionRequestSchema.safeParse({ emoji: '👍' }).success).toBe(true);
|
||||
expect(collabReactionRequestSchema.safeParse({ emoji: '' }).success).toBe(false);
|
||||
expect(collabReactionRequestSchema.safeParse({ emoji: '👍' }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(collabReactionRequestSchema.safeParse({ emoji: '' }).success).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,9 @@ export const collabNoteCreateRequestSchema = z.object({
|
||||
color: 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({
|
||||
title: z.string().optional(),
|
||||
@@ -28,7 +30,9 @@ export const collabNoteUpdateRequestSchema = z.object({
|
||||
pinned: z.union([z.boolean(), z.number()]).optional(),
|
||||
website: z.string().optional(),
|
||||
});
|
||||
export type CollabNoteUpdateRequest = z.infer<typeof collabNoteUpdateRequestSchema>;
|
||||
export type CollabNoteUpdateRequest = z.infer<
|
||||
typeof collabNoteUpdateRequestSchema
|
||||
>;
|
||||
|
||||
export const collabPollCreateRequestSchema = z.object({
|
||||
question: z.string().min(1),
|
||||
@@ -37,7 +41,9 @@ export const collabPollCreateRequestSchema = z.object({
|
||||
multiple_choice: z.boolean().optional(),
|
||||
deadline: z.string().optional(),
|
||||
});
|
||||
export type CollabPollCreateRequest = z.infer<typeof collabPollCreateRequestSchema>;
|
||||
export type CollabPollCreateRequest = z.infer<
|
||||
typeof collabPollCreateRequestSchema
|
||||
>;
|
||||
|
||||
export const collabPollVoteRequestSchema = z.object({
|
||||
option_index: z.number(),
|
||||
@@ -48,7 +54,9 @@ export const collabMessageCreateRequestSchema = z.object({
|
||||
text: z.string().min(1).max(5000),
|
||||
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({
|
||||
emoji: z.string().min(1),
|
||||
|
||||
@@ -1,30 +1,49 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
dayCreateRequestSchema,
|
||||
dayNoteCreateRequestSchema,
|
||||
dayNoteUpdateRequestSchema,
|
||||
} from './day.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('dayCreateRequestSchema', () => {
|
||||
it('accepts an optional date + notes', () => {
|
||||
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', () => {
|
||||
it('requires non-empty text capped at 500, time capped at 150', () => {
|
||||
expect(dayNoteCreateRequestSchema.safeParse({ text: 'Lunch' }).success).toBe(true);
|
||||
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);
|
||||
expect(
|
||||
dayNoteCreateRequestSchema.safeParse({ text: 'Lunch' }).success,
|
||||
).toBe(true);
|
||||
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', () => {
|
||||
it('allows omitting text and caps the lengths', () => {
|
||||
expect(dayNoteUpdateRequestSchema.safeParse({}).success).toBe(true);
|
||||
expect(dayNoteUpdateRequestSchema.safeParse({ icon: '🍽️' }).success).toBe(true);
|
||||
expect(dayNoteUpdateRequestSchema.safeParse({ text: 'x'.repeat(501) }).success).toBe(false);
|
||||
expect(dayNoteUpdateRequestSchema.safeParse({ icon: '🍽️' }).success).toBe(
|
||||
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 { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Day + day-note API contract — single source of truth for the
|
||||
* /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 { fileUpdateRequestSchema, fileLinkRequestSchema, photoVariantSchema } from './file.schema';
|
||||
|
||||
describe('fileUpdateRequestSchema', () => {
|
||||
it('accepts optional metadata, nullable ids, an empty body', () => {
|
||||
expect(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({ 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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fileLinkRequestSchema', () => {
|
||||
it('accepts any subset of reservation/assignment/place ids', () => {
|
||||
expect(fileLinkRequestSchema.safeParse({ reservation_id: 1 }).success).toBe(true);
|
||||
expect(fileLinkRequestSchema.safeParse({ assignment_id: '2', place_id: null }).success).toBe(true);
|
||||
expect(fileLinkRequestSchema.safeParse({ reservation_id: 1 }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
fileLinkRequestSchema.safeParse({ assignment_id: '2', place_id: null })
|
||||
.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.will1': 'Días, lugares y asignaciones por día',
|
||||
'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.will5': 'Tareas (sin asignar ni marcar)',
|
||||
'dashboard.confirm.copy.will6': 'Notas del día',
|
||||
|
||||
@@ -109,7 +109,7 @@ const dashboard: TranslationStrings = {
|
||||
'dashboard.mobile.currencyConverter': 'Convertisseur de devises',
|
||||
'dashboard.filter.planned': 'Planifiés',
|
||||
'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.badgeNext': 'À SUIVRE',
|
||||
'dashboard.hero.badgeRecent': 'RÉCENT',
|
||||
@@ -135,16 +135,17 @@ const dashboard: TranslationStrings = {
|
||||
'dashboard.atlas.acrossAllTrips': 'sur tous les voyages',
|
||||
'dashboard.atlas.distanceFlown': 'Distance parcourue en avion',
|
||||
'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.buddyOne': 'Compagnon',
|
||||
'dashboard.fx.from': 'De',
|
||||
'dashboard.fx.to': 'Vers',
|
||||
'dashboard.fx.unavailable': 'Taux indisponible',
|
||||
'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.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.willCopy': 'Sera copié',
|
||||
'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.wont4': 'Jetons de partage',
|
||||
'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.duplicate': 'Dupliquer',
|
||||
'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.
|
||||
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
|
||||
* dir must contain the exact same domain files as en/.
|
||||
|
||||
@@ -124,7 +124,7 @@ const dashboard: TranslationStrings = {
|
||||
'dashboard.hero.dayLeft': 'Giorno rimasto',
|
||||
'dashboard.hero.daysLeft': 'Giorni rimasti',
|
||||
'dashboard.hero.lastDay': 'Ultimo giorno',
|
||||
'dashboard.hero.untilStart': 'All\'inizio',
|
||||
'dashboard.hero.untilStart': "All'inizio",
|
||||
'dashboard.hero.startsIn': 'Si parte tra',
|
||||
'dashboard.atlas.countriesVisited': 'Atlas · Paesi visitati',
|
||||
'dashboard.atlas.ofTotal': 'di {total}',
|
||||
@@ -135,14 +135,15 @@ const dashboard: TranslationStrings = {
|
||||
'dashboard.atlas.acrossAllTrips': 'su tutti i viaggi',
|
||||
'dashboard.atlas.distanceFlown': 'Distanza in volo',
|
||||
'dashboard.atlas.kmUnit': 'km',
|
||||
'dashboard.atlas.aroundEquator': '≈ {count}× intorno all\'equatore',
|
||||
'dashboard.atlas.aroundEquator': "≈ {count}× intorno all'equatore",
|
||||
'dashboard.card.idea': 'Idea',
|
||||
'dashboard.card.buddyOne': 'Compagno',
|
||||
'dashboard.fx.from': 'Da',
|
||||
'dashboard.fx.to': 'A',
|
||||
'dashboard.fx.unavailable': 'Tasso non disponibile',
|
||||
'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.empty': 'Niente ancora prenotato.',
|
||||
'dashboard.confirm.copy.title': 'Copiare questo viaggio?',
|
||||
|
||||
@@ -153,14 +153,14 @@ const dashboard: TranslationStrings = {
|
||||
'dashboard.confirm.copy.wontCopy': 'Wordt niet gekopieerd',
|
||||
'dashboard.confirm.copy.wont1': 'Medewerkers & ledentoewijzingen',
|
||||
'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.confirm': 'Reis kopiëren',
|
||||
'dashboard.aria.toggleView': 'Weergave wisselen',
|
||||
'dashboard.aria.filter': 'Filter',
|
||||
'dashboard.aria.duplicate': 'Dupliceren',
|
||||
'dashboard.aria.refreshRates': 'Koersen vernieuwen',
|
||||
'dashboard.aria.swapCurrencies': 'Valuta\'s omwisselen',
|
||||
'dashboard.aria.swapCurrencies': "Valuta's omwisselen",
|
||||
'dashboard.aria.addTimezone': 'Tijdzone toevoegen',
|
||||
'dashboard.aria.removeTimezone': '{city} verwijderen',
|
||||
};
|
||||
|
||||
@@ -153,7 +153,8 @@ const dashboard: TranslationStrings = {
|
||||
'dashboard.fx.to': 'У',
|
||||
'dashboard.fx.unavailable': 'Курс недоступний',
|
||||
'dashboard.tz.searchPlaceholder': 'Пошук часового поясу…',
|
||||
'dashboard.tz.empty': 'Інших часових поясів поки немає — додайте за допомогою +',
|
||||
'dashboard.tz.empty':
|
||||
'Інших часових поясів поки немає — додайте за допомогою +',
|
||||
'dashboard.upcoming.title': 'Найближчі бронювання',
|
||||
'dashboard.upcoming.empty': 'Поки нічого не заброньовано.',
|
||||
'dashboard.aria.toggleView': 'Перемкнути вигляд',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
journeyCreateRequestSchema,
|
||||
journeyAddTripRequestSchema,
|
||||
@@ -8,48 +7,92 @@ import {
|
||||
journeyShareLinkRequestSchema,
|
||||
} from './journey.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('journeyCreateRequestSchema', () => {
|
||||
it('requires a title; subtitle + trip_ids optional', () => {
|
||||
expect(journeyCreateRequestSchema.safeParse({ title: 'Trip of a lifetime' }).success).toBe(true);
|
||||
expect(journeyCreateRequestSchema.safeParse({ title: 'X', trip_ids: [1, '2'] }).success).toBe(true);
|
||||
expect(journeyCreateRequestSchema.safeParse({ subtitle: 'no title' }).success).toBe(false);
|
||||
expect(
|
||||
journeyCreateRequestSchema.safeParse({ title: 'Trip of a lifetime' })
|
||||
.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', () => {
|
||||
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(true);
|
||||
expect(journeyAddTripRequestSchema.safeParse({ trip_id: 5 }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
journeyAddTripRequestSchema.safeParse({ trip_id: '5' }).success,
|
||||
).toBe(true);
|
||||
expect(journeyAddTripRequestSchema.safeParse({}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('journeyReorderEntriesRequestSchema', () => {
|
||||
it('requires a non-empty orderedIds array', () => {
|
||||
expect(journeyReorderEntriesRequestSchema.safeParse({ orderedIds: [3, 1, 2] }).success).toBe(true);
|
||||
expect(journeyReorderEntriesRequestSchema.safeParse({ orderedIds: [] }).success).toBe(false);
|
||||
expect(
|
||||
journeyReorderEntriesRequestSchema.safeParse({ orderedIds: [3, 1, 2] })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
journeyReorderEntriesRequestSchema.safeParse({ orderedIds: [] }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('journeyContributorRequestSchema', () => {
|
||||
it('requires user_id; role limited to editor/viewer', () => {
|
||||
expect(journeyContributorRequestSchema.safeParse({ user_id: 2 }).success).toBe(true);
|
||||
expect(journeyContributorRequestSchema.safeParse({ user_id: 2, role: 'editor' }).success).toBe(true);
|
||||
expect(journeyContributorRequestSchema.safeParse({ user_id: 2, role: 'admin' }).success).toBe(false);
|
||||
expect(
|
||||
journeyContributorRequestSchema.safeParse({ user_id: 2 }).success,
|
||||
).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', () => {
|
||||
it('requires a provider; accepts single asset_id or a batch', () => {
|
||||
expect(journeyProviderPhotosRequestSchema.safeParse({ 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);
|
||||
expect(
|
||||
journeyProviderPhotosRequestSchema.safeParse({
|
||||
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', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,13 +28,17 @@ export type JourneyAddTripRequest = z.infer<typeof journeyAddTripRequestSchema>;
|
||||
export const journeyReorderEntriesRequestSchema = z.object({
|
||||
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({
|
||||
user_id: z.union([z.string(), z.number()]),
|
||||
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({
|
||||
provider: z.string().min(1),
|
||||
@@ -43,11 +47,15 @@ export const journeyProviderPhotosRequestSchema = z.object({
|
||||
caption: 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({
|
||||
share_timeline: z.boolean().optional(),
|
||||
share_gallery: 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 {
|
||||
mapsSearchRequestSchema,
|
||||
mapsAutocompleteRequestSchema,
|
||||
@@ -6,34 +5,58 @@ import {
|
||||
mapsResolveUrlRequestSchema,
|
||||
} from './maps.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('mapsSearchRequestSchema', () => {
|
||||
it('requires a non-empty query', () => {
|
||||
expect(mapsSearchRequestSchema.safeParse({ query: 'berlin' }).success).toBe(true);
|
||||
expect(mapsSearchRequestSchema.safeParse({ query: '' }).success).toBe(false);
|
||||
expect(mapsSearchRequestSchema.safeParse({ query: 'berlin' }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(mapsSearchRequestSchema.safeParse({ query: '' }).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(mapsSearchRequestSchema.safeParse({}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapsAutocompleteRequestSchema', () => {
|
||||
it('caps input at 200 chars and allows an optional locationBias', () => {
|
||||
expect(mapsAutocompleteRequestSchema.safeParse({ input: 'be' }).success).toBe(true);
|
||||
expect(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);
|
||||
expect(
|
||||
mapsAutocompleteRequestSchema.safeParse({ input: 'be' }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
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', () => {
|
||||
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(mapsReverseQuerySchema.safeParse({ lat: '52.5' }).success).toBe(false);
|
||||
expect(
|
||||
mapsReverseQuerySchema.safeParse({ lat: '52.5', lng: '13.4' }).success,
|
||||
).toBe(true);
|
||||
expect(mapsReverseQuerySchema.safeParse({ lat: '52.5' }).success).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapsResolveUrlRequestSchema', () => {
|
||||
it('requires a non-empty url', () => {
|
||||
expect(mapsResolveUrlRequestSchema.safeParse({ url: 'https://maps.app.goo.gl/x' }).success).toBe(true);
|
||||
expect(mapsResolveUrlRequestSchema.safeParse({ url: '' }).success).toBe(false);
|
||||
expect(
|
||||
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(),
|
||||
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({
|
||||
lat: z.string().min(1),
|
||||
@@ -59,13 +61,17 @@ export const mapsAutocompleteResultSchema = z.object({
|
||||
suggestions: z.array(mapsAutocompleteSuggestionSchema),
|
||||
source: z.string(),
|
||||
});
|
||||
export type MapsAutocompleteResult = z.infer<typeof mapsAutocompleteResultSchema>;
|
||||
export type MapsAutocompleteResult = z.infer<
|
||||
typeof mapsAutocompleteResultSchema
|
||||
>;
|
||||
|
||||
export const mapsPlaceDetailsResultSchema = z.object({
|
||||
place: placeRecord.nullable(),
|
||||
disabled: z.boolean().optional(),
|
||||
});
|
||||
export type MapsPlaceDetailsResult = z.infer<typeof mapsPlaceDetailsResultSchema>;
|
||||
export type MapsPlaceDetailsResult = z.infer<
|
||||
typeof mapsPlaceDetailsResultSchema
|
||||
>;
|
||||
|
||||
export const mapsPlacePhotoResultSchema = z.object({
|
||||
photoUrl: z.string().nullable(),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
preferencesUpdateRequestSchema,
|
||||
notificationRespondRequestSchema,
|
||||
@@ -6,31 +5,55 @@ import {
|
||||
inAppListResultSchema,
|
||||
} from './notification.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('preferencesUpdateRequestSchema', () => {
|
||||
it('accepts a nested event/channel/enabled matrix', () => {
|
||||
expect(preferencesUpdateRequestSchema.safeParse({ trip_invite: { inapp: true, email: false } }).success).toBe(true);
|
||||
expect(preferencesUpdateRequestSchema.safeParse({ trip_invite: { inapp: 'yes' } }).success).toBe(false);
|
||||
expect(
|
||||
preferencesUpdateRequestSchema.safeParse({
|
||||
trip_invite: { inapp: true, email: false },
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
preferencesUpdateRequestSchema.safeParse({
|
||||
trip_invite: { inapp: 'yes' },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('notificationRespondRequestSchema', () => {
|
||||
it('only accepts positive/negative', () => {
|
||||
expect(notificationRespondRequestSchema.safeParse({ response: 'positive' }).success).toBe(true);
|
||||
expect(notificationRespondRequestSchema.safeParse({ response: 'maybe' }).success).toBe(false);
|
||||
expect(
|
||||
notificationRespondRequestSchema.safeParse({ response: 'positive' })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
notificationRespondRequestSchema.safeParse({ response: 'maybe' }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('channelTestResultSchema', () => {
|
||||
it('accepts a success result and an error result', () => {
|
||||
expect(channelTestResultSchema.safeParse({ success: true }).success).toBe(true);
|
||||
expect(channelTestResultSchema.safeParse({ success: false, error: 'SMTP down' }).success).toBe(true);
|
||||
expect(channelTestResultSchema.safeParse({ success: true }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
channelTestResultSchema.safeParse({ success: false, error: 'SMTP down' })
|
||||
.success,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inAppListResultSchema', () => {
|
||||
it('accepts the list envelope with open notification rows', () => {
|
||||
expect(inAppListResultSchema.safeParse({
|
||||
notifications: [{ id: 1, type: 'info', anything: 'goes' }], total: 1, unread_count: 0,
|
||||
}).success).toBe(true);
|
||||
expect(
|
||||
inAppListResultSchema.safeParse({
|
||||
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.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 testWebhookRequestSchema = z.object({ url: z.string().optional() });
|
||||
export const testWebhookRequestSchema = z.object({
|
||||
url: z.string().optional(),
|
||||
});
|
||||
export const testNtfyRequestSchema = z.object({
|
||||
topic: z.string().optional(),
|
||||
server: z.string().optional(),
|
||||
@@ -39,7 +43,9 @@ export type ChannelTestResult = z.infer<typeof channelTestResultSchema>;
|
||||
export const notificationRespondRequestSchema = z.object({
|
||||
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). */
|
||||
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 { oauthTokenRequestSchema, oauthConsentRequestSchema, oauthClientCreateRequestSchema } from './oauth.schema';
|
||||
|
||||
describe('oauthTokenRequestSchema', () => {
|
||||
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(oauthTokenRequestSchema.safeParse({ grant_type: 'client_credentials', client_id: 'c', client_secret: 's', scope: 'a b' }).success).toBe(true);
|
||||
expect(
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('oauthConsentRequestSchema', () => {
|
||||
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(oauthConsentRequestSchema.safeParse({ client_id: 'c', redirect_uri: 'u', scope: 's', code_challenge: 'cc', code_challenge_method: 'S256' }).success).toBe(false);
|
||||
expect(
|
||||
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', () => {
|
||||
it('requires name + allowed_scopes', () => {
|
||||
expect(oauthClientCreateRequestSchema.safeParse({ 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);
|
||||
expect(
|
||||
oauthClientCreateRequestSchema.safeParse({
|
||||
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()),
|
||||
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 { oidcCallbackQuerySchema, oidcExchangeQuerySchema } from './oidc.schema';
|
||||
|
||||
describe('oidcCallbackQuerySchema', () => {
|
||||
it('accepts code+state, an error, or nothing (all optional)', () => {
|
||||
expect(oidcCallbackQuerySchema.safeParse({ code: 'c', state: 's' }).success).toBe(true);
|
||||
expect(oidcCallbackQuerySchema.safeParse({ error: 'access_denied' }).success).toBe(true);
|
||||
expect(
|
||||
oidcCallbackQuerySchema.safeParse({ code: 'c', state: 's' }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
oidcCallbackQuerySchema.safeParse({ error: 'access_denied' }).success,
|
||||
).toBe(true);
|
||||
expect(oidcCallbackQuerySchema.safeParse({}).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
packingCreateItemRequestSchema,
|
||||
packingImportRequestSchema,
|
||||
@@ -6,30 +5,52 @@ import {
|
||||
packingSaveTemplateRequestSchema,
|
||||
} from './packing.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('packingCreateItemRequestSchema', () => {
|
||||
it('requires a non-empty name; category/checked optional', () => {
|
||||
expect(packingCreateItemRequestSchema.safeParse({ name: 'Socks' }).success).toBe(true);
|
||||
expect(packingCreateItemRequestSchema.safeParse({ name: 'Socks', category: 'Clothes', checked: true }).success).toBe(true);
|
||||
expect(packingCreateItemRequestSchema.safeParse({ name: '' }).success).toBe(false);
|
||||
expect(
|
||||
packingCreateItemRequestSchema.safeParse({ name: 'Socks' }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
packingCreateItemRequestSchema.safeParse({
|
||||
name: 'Socks',
|
||||
category: 'Clothes',
|
||||
checked: true,
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(packingCreateItemRequestSchema.safeParse({ name: '' }).success).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('packingImportRequestSchema', () => {
|
||||
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', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('packingSaveTemplateRequestSchema', () => {
|
||||
it('requires a name', () => {
|
||||
expect(packingSaveTemplateRequestSchema.safeParse({ name: 'Summer' }).success).toBe(true);
|
||||
expect(packingSaveTemplateRequestSchema.safeParse({ name: '' }).success).toBe(false);
|
||||
expect(
|
||||
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(),
|
||||
checked: z.boolean().optional(),
|
||||
});
|
||||
export type PackingCreateItemRequest = z.infer<typeof packingCreateItemRequestSchema>;
|
||||
export type PackingCreateItemRequest = z.infer<
|
||||
typeof packingCreateItemRequestSchema
|
||||
>;
|
||||
|
||||
export const packingUpdateItemRequestSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
@@ -78,7 +80,9 @@ export const packingUpdateItemRequestSchema = z.object({
|
||||
bag_id: z.number().nullable().optional(),
|
||||
quantity: z.number().optional(),
|
||||
});
|
||||
export type PackingUpdateItemRequest = z.infer<typeof packingUpdateItemRequestSchema>;
|
||||
export type PackingUpdateItemRequest = z.infer<
|
||||
typeof packingUpdateItemRequestSchema
|
||||
>;
|
||||
|
||||
export const packingImportRequestSchema = z.object({
|
||||
items: z.array(open),
|
||||
@@ -94,7 +98,9 @@ export const packingCreateBagRequestSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
color: z.string().optional(),
|
||||
});
|
||||
export type PackingCreateBagRequest = z.infer<typeof packingCreateBagRequestSchema>;
|
||||
export type PackingCreateBagRequest = z.infer<
|
||||
typeof packingCreateBagRequestSchema
|
||||
>;
|
||||
|
||||
export const packingUpdateBagRequestSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
@@ -102,19 +108,27 @@ export const packingUpdateBagRequestSchema = z.object({
|
||||
weight_limit_grams: 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({
|
||||
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({
|
||||
name: z.string().min(1),
|
||||
});
|
||||
export type PackingSaveTemplateRequest = z.infer<typeof packingSaveTemplateRequestSchema>;
|
||||
export type PackingSaveTemplateRequest = z.infer<
|
||||
typeof packingSaveTemplateRequestSchema
|
||||
>;
|
||||
|
||||
export const packingCategoryAssigneesRequestSchema = z.object({
|
||||
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 {
|
||||
placeCreateRequestSchema,
|
||||
placeBulkDeleteRequestSchema,
|
||||
placeImportListRequestSchema,
|
||||
} from './place.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('placeCreateRequestSchema', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('placeBulkDeleteRequestSchema', () => {
|
||||
it('requires a numeric ids array', () => {
|
||||
expect(placeBulkDeleteRequestSchema.safeParse({ ids: [1, 2] }).success).toBe(true);
|
||||
expect(placeBulkDeleteRequestSchema.safeParse({ ids: ['a'] }).success).toBe(false);
|
||||
expect(
|
||||
placeBulkDeleteRequestSchema.safeParse({ ids: [1, 2] }).success,
|
||||
).toBe(true);
|
||||
expect(placeBulkDeleteRequestSchema.safeParse({ ids: ['a'] }).success).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('placeImportListRequestSchema', () => {
|
||||
it('requires a non-empty url', () => {
|
||||
expect(placeImportListRequestSchema.safeParse({ url: 'http://x' }).success).toBe(true);
|
||||
expect(placeImportListRequestSchema.safeParse({ url: '' }).success).toBe(false);
|
||||
expect(
|
||||
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 { z } from 'zod';
|
||||
|
||||
/**
|
||||
* 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).
|
||||
@@ -100,7 +100,9 @@ export const assignmentPlaceSchema = z.object({
|
||||
});
|
||||
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 const placeUpdateRequestSchema = open;
|
||||
@@ -109,12 +111,16 @@ export type PlaceUpdateRequest = z.infer<typeof placeUpdateRequestSchema>;
|
||||
export const placeBulkDeleteRequestSchema = z.object({
|
||||
ids: z.array(z.number()),
|
||||
});
|
||||
export type PlaceBulkDeleteRequest = z.infer<typeof placeBulkDeleteRequestSchema>;
|
||||
export type PlaceBulkDeleteRequest = z.infer<
|
||||
typeof placeBulkDeleteRequestSchema
|
||||
>;
|
||||
|
||||
export const placeImportListRequestSchema = z.object({
|
||||
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. */
|
||||
export const placeListQuerySchema = z.object({
|
||||
|
||||
@@ -1,27 +1,52 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
reservationCreateRequestSchema,
|
||||
reservationPositionsRequestSchema,
|
||||
accommodationCreateRequestSchema,
|
||||
} from './reservation.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('reservationCreateRequestSchema', () => {
|
||||
it('requires a title and keeps the other booking fields open', () => {
|
||||
expect(reservationCreateRequestSchema.safeParse({ title: 'Hotel', anything: 1, metadata: {} }).success).toBe(true);
|
||||
expect(reservationCreateRequestSchema.safeParse({ location: 'x' }).success).toBe(false);
|
||||
expect(
|
||||
reservationCreateRequestSchema.safeParse({
|
||||
title: 'Hotel',
|
||||
anything: 1,
|
||||
metadata: {},
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
reservationCreateRequestSchema.safeParse({ location: 'x' }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reservationPositionsRequestSchema', () => {
|
||||
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(reservationPositionsRequestSchema.safeParse({ positions: [{ id: 1 }] }).success).toBe(false);
|
||||
expect(
|
||||
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', () => {
|
||||
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(accommodationCreateRequestSchema.safeParse({ place_id: 2 }).success).toBe(false);
|
||||
expect(
|
||||
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>;
|
||||
|
||||
/** Reservation create: title is required; the many optional fields stay open. */
|
||||
export const reservationCreateRequestSchema = open.and(z.object({ title: z.string().min(1) }));
|
||||
export type ReservationCreateRequest = z.infer<typeof reservationCreateRequestSchema>;
|
||||
export const reservationCreateRequestSchema = open.and(
|
||||
z.object({ title: z.string().min(1) }),
|
||||
);
|
||||
export type ReservationCreateRequest = z.infer<
|
||||
typeof reservationCreateRequestSchema
|
||||
>;
|
||||
|
||||
export const reservationUpdateRequestSchema = open;
|
||||
export type ReservationUpdateRequest = z.infer<typeof reservationUpdateRequestSchema>;
|
||||
export type ReservationUpdateRequest = z.infer<
|
||||
typeof reservationUpdateRequestSchema
|
||||
>;
|
||||
|
||||
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(),
|
||||
});
|
||||
export type ReservationPositionsRequest = z.infer<typeof reservationPositionsRequestSchema>;
|
||||
export type ReservationPositionsRequest = z.infer<
|
||||
typeof reservationPositionsRequestSchema
|
||||
>;
|
||||
|
||||
export const accommodationCreateRequestSchema = z.object({
|
||||
place_id: z.union([z.number(), z.string()]),
|
||||
@@ -123,7 +133,11 @@ export const accommodationCreateRequestSchema = z.object({
|
||||
confirmation: 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 type AccommodationUpdateRequest = z.infer<typeof accommodationUpdateRequestSchema>;
|
||||
export type AccommodationUpdateRequest = z.infer<
|
||||
typeof accommodationUpdateRequestSchema
|
||||
>;
|
||||
|
||||
@@ -1,118 +1,139 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { sanitizeInlineHtml, sanitizeRichTextHtml, escapeHtml } from './sanitize'
|
||||
import {
|
||||
sanitizeInlineHtml,
|
||||
sanitizeRichTextHtml,
|
||||
escapeHtml,
|
||||
} from './sanitize';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('escapeHtml', () => {
|
||||
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)', () => {
|
||||
expect(escapeHtml('<')).toBe('&lt;')
|
||||
})
|
||||
expect(escapeHtml('<')).toBe('&lt;');
|
||||
});
|
||||
|
||||
it('returns empty string for empty input', () => {
|
||||
expect(escapeHtml('')).toBe('')
|
||||
})
|
||||
expect(escapeHtml('')).toBe('');
|
||||
});
|
||||
|
||||
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', () => {
|
||||
expect(escapeHtml('<script>alert(1)</script>')).toBe('<script>alert(1)</script>')
|
||||
})
|
||||
})
|
||||
expect(escapeHtml('<script>alert(1)</script>')).toBe(
|
||||
'<script>alert(1)</script>',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeInlineHtml', () => {
|
||||
it('returns empty string for empty input', () => {
|
||||
expect(sanitizeInlineHtml('')).toBe('')
|
||||
})
|
||||
expect(sanitizeInlineHtml('')).toBe('');
|
||||
});
|
||||
|
||||
it('preserves the allowed inline tags', () => {
|
||||
expect(sanitizeInlineHtml('a <strong>b</strong> c')).toBe('a <strong>b</strong> c')
|
||||
expect(sanitizeInlineHtml('<em>x</em>')).toBe('<em>x</em>')
|
||||
})
|
||||
expect(sanitizeInlineHtml('a <strong>b</strong> c')).toBe(
|
||||
'a <strong>b</strong> c',
|
||||
);
|
||||
expect(sanitizeInlineHtml('<em>x</em>')).toBe('<em>x</em>');
|
||||
});
|
||||
|
||||
it('strips <script> entirely', () => {
|
||||
const out = sanitizeInlineHtml('safe <script>alert(1)</script> text')
|
||||
expect(out).not.toContain('<script')
|
||||
expect(out).not.toContain('alert(1)')
|
||||
expect(out).toContain('safe')
|
||||
})
|
||||
const out = sanitizeInlineHtml('safe <script>alert(1)</script> text');
|
||||
expect(out).not.toContain('<script');
|
||||
expect(out).not.toContain('alert(1)');
|
||||
expect(out).toContain('safe');
|
||||
});
|
||||
|
||||
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', () => {
|
||||
const out = sanitizeInlineHtml('<span onclick="alert(1)">hi</span>')
|
||||
expect(out).not.toContain('onclick')
|
||||
expect(out).toContain('hi')
|
||||
})
|
||||
const out = sanitizeInlineHtml('<span onclick="alert(1)">hi</span>');
|
||||
expect(out).not.toContain('onclick');
|
||||
expect(out).toContain('hi');
|
||||
});
|
||||
|
||||
it('strips style attribute (CSS-injection surface)', () => {
|
||||
const out = sanitizeInlineHtml('<span style="background:url(javascript:alert(1))">x</span>')
|
||||
expect(out).not.toContain('style=')
|
||||
expect(out).not.toContain('javascript:')
|
||||
})
|
||||
const out = sanitizeInlineHtml(
|
||||
'<span style="background:url(javascript:alert(1))">x</span>',
|
||||
);
|
||||
expect(out).not.toContain('style=');
|
||||
expect(out).not.toContain('javascript:');
|
||||
});
|
||||
|
||||
it('strips iframe / object / embed / svg-with-script', () => {
|
||||
expect(sanitizeInlineHtml('<iframe src="evil"></iframe>')).toBe('')
|
||||
expect(sanitizeInlineHtml('<object data="evil"></object>')).toBe('')
|
||||
expect(sanitizeInlineHtml('<embed src="evil" />')).toBe('')
|
||||
expect(sanitizeInlineHtml('<svg><script>alert(1)</script></svg>')).not.toContain('script')
|
||||
})
|
||||
expect(sanitizeInlineHtml('<iframe src="evil"></iframe>')).toBe('');
|
||||
expect(sanitizeInlineHtml('<object data="evil"></object>')).toBe('');
|
||||
expect(sanitizeInlineHtml('<embed src="evil" />')).toBe('');
|
||||
expect(
|
||||
sanitizeInlineHtml('<svg><script>alert(1)</script></svg>'),
|
||||
).not.toContain('script');
|
||||
});
|
||||
|
||||
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.
|
||||
const out = sanitizeInlineHtml('<a href="javascript:alert(1)">x</a>')
|
||||
expect(out).toBe('x')
|
||||
})
|
||||
const out = sanitizeInlineHtml('<a href="javascript:alert(1)">x</a>');
|
||||
expect(out).toBe('x');
|
||||
});
|
||||
|
||||
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', () => {
|
||||
it('preserves the full prose tag set', () => {
|
||||
const html = '<p>hello <strong>world</strong></p><ul><li>one</li></ul>'
|
||||
const out = sanitizeRichTextHtml(html)
|
||||
expect(out).toContain('<p>')
|
||||
expect(out).toContain('<strong>world</strong>')
|
||||
expect(out).toContain('<li>one</li>')
|
||||
})
|
||||
const html = '<p>hello <strong>world</strong></p><ul><li>one</li></ul>';
|
||||
const out = sanitizeRichTextHtml(html);
|
||||
expect(out).toContain('<p>');
|
||||
expect(out).toContain('<strong>world</strong>');
|
||||
expect(out).toContain('<li>one</li>');
|
||||
});
|
||||
|
||||
it('still strips <script> + on* + style', () => {
|
||||
const out = sanitizeRichTextHtml('<p onclick="alert(1)" style="x">hi</p><script>x()</script>')
|
||||
expect(out).not.toContain('onclick')
|
||||
expect(out).not.toContain('style=')
|
||||
expect(out).not.toContain('<script')
|
||||
})
|
||||
const out = sanitizeRichTextHtml(
|
||||
'<p onclick="alert(1)" style="x">hi</p><script>x()</script>',
|
||||
);
|
||||
expect(out).not.toContain('onclick');
|
||||
expect(out).not.toContain('style=');
|
||||
expect(out).not.toContain('<script');
|
||||
});
|
||||
|
||||
it('blocks javascript: hrefs', () => {
|
||||
const out = sanitizeRichTextHtml('<a href="javascript:alert(1)">x</a>')
|
||||
expect(out).not.toContain('javascript:')
|
||||
})
|
||||
const out = sanitizeRichTextHtml('<a href="javascript:alert(1)">x</a>');
|
||||
expect(out).not.toContain('javascript:');
|
||||
});
|
||||
|
||||
it('blocks data: hrefs that smuggle scripts', () => {
|
||||
const out = sanitizeRichTextHtml('<a href="data:text/html,<script>alert(1)</script>">x</a>')
|
||||
expect(out).not.toContain('data:text/html')
|
||||
})
|
||||
const out = sanitizeRichTextHtml(
|
||||
'<a href="data:text/html,<script>alert(1)</script>">x</a>',
|
||||
);
|
||||
expect(out).not.toContain('data:text/html');
|
||||
});
|
||||
|
||||
it('keeps http(s) hrefs intact', () => {
|
||||
const out = sanitizeRichTextHtml('<a href="https://example.com">link</a>')
|
||||
expect(out).toContain('href="https://example.com"')
|
||||
})
|
||||
const out = sanitizeRichTextHtml('<a href="https://example.com">link</a>');
|
||||
expect(out).toContain('href="https://example.com"');
|
||||
});
|
||||
|
||||
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', () => {
|
||||
const mathPayload = '<math><mtext><script>alert(1)</script></mtext></math>'
|
||||
const out = sanitizeRichTextHtml(mathPayload)
|
||||
expect(out).not.toContain('<script')
|
||||
expect(out).not.toContain('alert(1)')
|
||||
})
|
||||
})
|
||||
const mathPayload = '<math><mtext><script>alert(1)</script></mtext></math>';
|
||||
const out = sanitizeRichTextHtml(mathPayload);
|
||||
expect(out).not.toContain('<script');
|
||||
expect(out).not.toContain('alert(1)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import DOMPurify from 'isomorphic-dompurify'
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
|
||||
/**
|
||||
* 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
|
||||
// and rely on `sanitizeRichTextHtml` when a richer surface needs full prose.
|
||||
const INLINE_TAGS = [
|
||||
'b', 'strong', 'i', 'em', 'u', 's', 'del', 'ins',
|
||||
'mark', 'code', 'sub', 'sup', 'br', 'span',
|
||||
] as const
|
||||
'b',
|
||||
'strong',
|
||||
'i',
|
||||
'em',
|
||||
'u',
|
||||
's',
|
||||
'del',
|
||||
'ins',
|
||||
'mark',
|
||||
'code',
|
||||
'sub',
|
||||
'sup',
|
||||
'br',
|
||||
'span',
|
||||
] as const;
|
||||
|
||||
const FULL_TAGS = [
|
||||
...INLINE_TAGS,
|
||||
'p', 'div', 'ul', 'ol', 'li',
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||
'blockquote', 'pre', 'hr', 'a',
|
||||
] as const
|
||||
'p',
|
||||
'div',
|
||||
'ul',
|
||||
'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
|
||||
@@ -45,7 +69,7 @@ export function escapeHtml(value: string): string {
|
||||
.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.
|
||||
*/
|
||||
export function sanitizeInlineHtml(html: string): string {
|
||||
if (!html) return ''
|
||||
if (!html) return '';
|
||||
return DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: [...INLINE_TAGS],
|
||||
ALLOWED_ATTR: [],
|
||||
KEEP_CONTENT: true,
|
||||
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.
|
||||
*/
|
||||
export function sanitizeRichTextHtml(html: string): string {
|
||||
if (!html) return ''
|
||||
if (!html) return '';
|
||||
return DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: [...FULL_TAGS],
|
||||
ALLOWED_ATTR: [...SAFE_ATTRIBUTES],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,18 +1,35 @@
|
||||
import {
|
||||
settingUpsertRequestSchema,
|
||||
settingsBulkRequestSchema,
|
||||
MASKED_SETTING_VALUE,
|
||||
} from './settings.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { settingUpsertRequestSchema, settingsBulkRequestSchema, MASKED_SETTING_VALUE } from './settings.schema';
|
||||
|
||||
describe('settingUpsertRequestSchema', () => {
|
||||
it('requires a key; value is any/optional', () => {
|
||||
expect(settingUpsertRequestSchema.safeParse({ key: 'theme', value: 'dark' }).success).toBe(true);
|
||||
expect(settingUpsertRequestSchema.safeParse({ key: 'theme' }).success).toBe(true);
|
||||
expect(settingUpsertRequestSchema.safeParse({ value: 'dark' }).success).toBe(false);
|
||||
expect(
|
||||
settingUpsertRequestSchema.safeParse({ key: 'theme', value: 'dark' })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(settingUpsertRequestSchema.safeParse({ key: 'theme' }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
settingUpsertRequestSchema.safeParse({ value: 'dark' }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('settingsBulkRequestSchema', () => {
|
||||
it('requires a settings record', () => {
|
||||
expect(settingsBulkRequestSchema.safeParse({ settings: { a: 1, b: 'x' } }).success).toBe(true);
|
||||
expect(settingsBulkRequestSchema.safeParse({ settings: {} }).success).toBe(true);
|
||||
expect(
|
||||
settingsBulkRequestSchema.safeParse({ settings: { a: 1, b: 'x' } })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(settingsBulkRequestSchema.safeParse({ settings: {} }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(settingsBulkRequestSchema.safeParse({}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { shareLinkRequestSchema } from './share.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('shareLinkRequestSchema', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
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 { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('systemNoticeDtoSchema', () => {
|
||||
it('accepts a minimal notice (required fields only)', () => {
|
||||
const parsed = systemNoticeDtoSchema.parse({
|
||||
id: 'welcome', display: 'modal', severity: 'info',
|
||||
titleKey: 'notice.welcome.title', bodyKey: 'notice.welcome.body', dismissible: true,
|
||||
id: 'welcome',
|
||||
display: 'modal',
|
||||
severity: 'info',
|
||||
titleKey: 'notice.welcome.title',
|
||||
bodyKey: 'notice.welcome.body',
|
||||
dismissible: true,
|
||||
});
|
||||
expect(parsed.id).toBe('welcome');
|
||||
});
|
||||
|
||||
it('accepts a rich notice with media, highlights and a nav CTA', () => {
|
||||
expect(systemNoticeDtoSchema.safeParse({
|
||||
id: 'release', display: 'banner', severity: 'warn',
|
||||
titleKey: 't', bodyKey: 'b', dismissible: false,
|
||||
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);
|
||||
expect(
|
||||
systemNoticeDtoSchema.safeParse({
|
||||
id: 'release',
|
||||
display: 'banner',
|
||||
severity: 'warn',
|
||||
titleKey: 't',
|
||||
bodyKey: 'b',
|
||||
dismissible: false,
|
||||
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', () => {
|
||||
expect(systemNoticeDtoSchema.safeParse({
|
||||
id: 'x', display: 'toast', severity: 'critical',
|
||||
titleKey: 't', bodyKey: 'b', dismissible: true,
|
||||
cta: { kind: 'action', labelKey: 'do', actionId: 'reload', dismissOnAction: true },
|
||||
}).success).toBe(true);
|
||||
expect(
|
||||
systemNoticeDtoSchema.safeParse({
|
||||
id: 'x',
|
||||
display: 'toast',
|
||||
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', () => {
|
||||
expect(systemNoticeDtoSchema.safeParse({
|
||||
id: 'x', display: 'popup', severity: 'info', titleKey: 't', 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);
|
||||
expect(
|
||||
systemNoticeDtoSchema.safeParse({
|
||||
id: 'x',
|
||||
display: 'popup',
|
||||
severity: 'info',
|
||||
titleKey: 't',
|
||||
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 { tagSchema, createTagRequestSchema, updateTagRequestSchema } from './tag.schema';
|
||||
|
||||
describe('tagSchema', () => {
|
||||
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', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,30 +1,51 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
todoCreateItemRequestSchema,
|
||||
todoUpdateItemRequestSchema,
|
||||
todoReorderRequestSchema,
|
||||
} from './todo.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('todoCreateItemRequestSchema', () => {
|
||||
it('requires a name; metadata optional with the service shapes', () => {
|
||||
expect(todoCreateItemRequestSchema.safeParse({ name: 'Book hotel' }).success).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);
|
||||
expect(
|
||||
todoCreateItemRequestSchema.safeParse({ name: 'Book hotel' }).success,
|
||||
).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
|
||||
expect(todoCreateItemRequestSchema.safeParse({ name: 'X', priority: 'high' }).success).toBe(false);
|
||||
expect(
|
||||
todoCreateItemRequestSchema.safeParse({ name: 'X', priority: 'high' })
|
||||
.success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('todoUpdateItemRequestSchema', () => {
|
||||
it('allows every field to be omitted and accepts checked', () => {
|
||||
expect(todoUpdateItemRequestSchema.safeParse({}).success).toBe(true);
|
||||
expect(todoUpdateItemRequestSchema.safeParse({ checked: true }).success).toBe(true);
|
||||
expect(
|
||||
todoUpdateItemRequestSchema.safeParse({ checked: true }).success,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('todoReorderRequestSchema', () => {
|
||||
it('requires an array of numeric ids', () => {
|
||||
expect(todoReorderRequestSchema.safeParse({ orderedIds: [1, 2, 3] }).success).toBe(true);
|
||||
expect(todoReorderRequestSchema.safeParse({ orderedIds: ['a'] }).success).toBe(false);
|
||||
expect(
|
||||
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({
|
||||
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 {
|
||||
tripCreateRequestSchema,
|
||||
tripUpdateRequestSchema,
|
||||
tripAddMemberRequestSchema,
|
||||
} from './trip.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('tripCreateRequestSchema', () => {
|
||||
it('requires a title; dates/currency/reminder optional', () => {
|
||||
expect(tripCreateRequestSchema.safeParse({ title: 'Japan' }).success).toBe(true);
|
||||
expect(tripCreateRequestSchema.safeParse({ title: 'Japan', start_date: '2026-07-01', day_count: 7 }).success).toBe(true);
|
||||
expect(tripCreateRequestSchema.safeParse({ title: 'Japan' }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
tripCreateRequestSchema.safeParse({
|
||||
title: 'Japan',
|
||||
start_date: '2026-07-01',
|
||||
day_count: 7,
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(tripCreateRequestSchema.safeParse({}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -16,13 +25,18 @@ describe('tripCreateRequestSchema', () => {
|
||||
describe('tripUpdateRequestSchema', () => {
|
||||
it('is fully partial and accepts is_archived + cover_image', () => {
|
||||
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', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
vacayAddHolidayCalendarRequestSchema,
|
||||
vacayInviteRequestSchema,
|
||||
@@ -6,34 +5,61 @@ import {
|
||||
vacayAddYearRequestSchema,
|
||||
} from './vacay.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('vacayAddHolidayCalendarRequestSchema', () => {
|
||||
it('requires a region; label/color/sort_order optional', () => {
|
||||
expect(vacayAddHolidayCalendarRequestSchema.safeParse({ region: 'DE-BY' }).success).toBe(true);
|
||||
expect(vacayAddHolidayCalendarRequestSchema.safeParse({ region: 'DE-BY', label: null }).success).toBe(true);
|
||||
expect(vacayAddHolidayCalendarRequestSchema.safeParse({}).success).toBe(false);
|
||||
expect(
|
||||
vacayAddHolidayCalendarRequestSchema.safeParse({ region: 'DE-BY' })
|
||||
.success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
vacayAddHolidayCalendarRequestSchema.safeParse({
|
||||
region: 'DE-BY',
|
||||
label: null,
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(vacayAddHolidayCalendarRequestSchema.safeParse({}).success).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('vacayInviteRequestSchema', () => {
|
||||
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(true);
|
||||
expect(vacayInviteRequestSchema.safeParse({ user_id: 2 }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(vacayInviteRequestSchema.safeParse({ user_id: '2' }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(vacayInviteRequestSchema.safeParse({}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('vacayToggleEntryRequestSchema', () => {
|
||||
it('requires a date; target_user_id optional', () => {
|
||||
expect(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({ 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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('vacayAddYearRequestSchema', () => {
|
||||
it('accepts a numeric or string year', () => {
|
||||
expect(vacayAddYearRequestSchema.safeParse({ year: 2027 }).success).toBe(true);
|
||||
expect(vacayAddYearRequestSchema.safeParse({ year: '2027' }).success).toBe(true);
|
||||
expect(vacayAddYearRequestSchema.safeParse({ year: 2027 }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(vacayAddYearRequestSchema.safeParse({ year: '2027' }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(vacayAddYearRequestSchema.safeParse({}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,9 @@ export const vacayAddHolidayCalendarRequestSchema = z.object({
|
||||
color: z.string().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({
|
||||
color: z.string().optional(),
|
||||
@@ -38,7 +40,9 @@ export type VacayInviteRequest = z.infer<typeof vacayInviteRequestSchema>;
|
||||
export const vacayInviteActionRequestSchema = z.object({
|
||||
plan_id: z.number().optional(),
|
||||
});
|
||||
export type VacayInviteActionRequest = z.infer<typeof vacayInviteActionRequestSchema>;
|
||||
export type VacayInviteActionRequest = z.infer<
|
||||
typeof vacayInviteActionRequestSchema
|
||||
>;
|
||||
|
||||
export const vacayAddYearRequestSchema = z.object({
|
||||
year: z.union([z.number(), z.string()]),
|
||||
@@ -49,19 +53,25 @@ export const vacayToggleEntryRequestSchema = z.object({
|
||||
date: z.string().min(1),
|
||||
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({
|
||||
date: z.string(),
|
||||
note: z.string().optional(),
|
||||
});
|
||||
export type VacayCompanyHolidayRequest = z.infer<typeof vacayCompanyHolidayRequestSchema>;
|
||||
export type VacayCompanyHolidayRequest = z.infer<
|
||||
typeof vacayCompanyHolidayRequestSchema
|
||||
>;
|
||||
|
||||
export const vacayUpdateStatsRequestSchema = z.object({
|
||||
vacation_days: z.number().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. */
|
||||
export const vacayPlanDataSchema = open;
|
||||
|
||||
Reference in New Issue
Block a user