Files
pantree/src/test/validators.test.ts
Azriel e633d693da feat: implement backend API for Pantree Phase 1 MVP
- Project setup: package.json, tsconfig.json, jest.config.js, .env.example
- Config: env.ts, constants.ts (ALLOWED_UNITS, bcrypt rounds, JWT, deletion windows)
- DB: Knex connection, knexfile, migrations 001-006 (users, password_reset_tokens,
  pantry_items, recipes+recipe_ingredients, shopping_lists+items, deleted_records)
- Seeds: 10 seeded recipes with full ingredient lists
- Middleware: JWT authMiddleware (validates token + user existence), errorHandler
- Utils: Pino logger, JWT sign helper, Zod validators for all request shapes
- Services: authService (signup/signin/google/pwd-reset/soft-delete/restore),
  pantryService (CRUD + case-insensitive duplicate guard),
  recipeService (browse+filter+scale), shoppingListService (CRUD+merge logic),
  syncService (delta sync + tombstone cleanup), emailService (SendGrid)
- Routes: /v1/auth, /v1/pantry, /v1/recipes, /v1/shopping-lists, /v1/sync
- App: Express factory (createApp), server entry point
- Jobs: node-cron daily hard-delete + tombstone cleanup
- Tests: validators, utils, auth, pantry, recipes, shopping lists, sync
2026-05-10 15:00:15 +00:00

159 lines
5.6 KiB
TypeScript

import { signupSchema, signinSchema, addPantryItemSchema, updatePantryItemSchema, addShoppingListItemSchema, updateShoppingListItemSchema, addRecipesToListSchema, recipeQuerySchema, syncQuerySchema } from '../../utils/validators';
describe('Validators', () => {
describe('signupSchema', () => {
it('accepts valid signup data', () => {
const result = signupSchema.safeParse({ email: 'user@example.com', password: 'password123', name: 'Jane' });
expect(result.success).toBe(true);
});
it('rejects invalid email', () => {
const result = signupSchema.safeParse({ email: 'not-an-email', password: 'password123', name: 'Jane' });
expect(result.success).toBe(false);
});
it('rejects short password', () => {
const result = signupSchema.safeParse({ email: 'user@example.com', password: 'short', name: 'Jane' });
expect(result.success).toBe(false);
});
it('rejects non-alphanumeric password', () => {
const result = signupSchema.safeParse({ email: 'user@example.com', password: 'pass!word', name: 'Jane' });
expect(result.success).toBe(false);
});
it('rejects empty name', () => {
const result = signupSchema.safeParse({ email: 'user@example.com', password: 'password123', name: '' });
expect(result.success).toBe(false);
});
});
describe('addPantryItemSchema', () => {
it('accepts valid pantry item', () => {
const result = addPantryItemSchema.safeParse({ item_name: 'Flour', quantity: 5 });
expect(result.success).toBe(true);
});
it('rejects zero quantity', () => {
const result = addPantryItemSchema.safeParse({ item_name: 'Flour', quantity: 0 });
expect(result.success).toBe(false);
});
it('rejects negative quantity', () => {
const result = addPantryItemSchema.safeParse({ item_name: 'Flour', quantity: -1 });
expect(result.success).toBe(false);
});
it('rejects fractional quantity', () => {
const result = addPantryItemSchema.safeParse({ item_name: 'Flour', quantity: 1.5 });
expect(result.success).toBe(false);
});
it('rejects empty item name', () => {
const result = addPantryItemSchema.safeParse({ item_name: '', quantity: 5 });
expect(result.success).toBe(false);
});
});
describe('updatePantryItemSchema', () => {
it('accepts valid update', () => {
const result = updatePantryItemSchema.safeParse({ quantity: 3 });
expect(result.success).toBe(true);
});
it('accepts update with last_modified', () => {
const result = updatePantryItemSchema.safeParse({ quantity: 3, last_modified: '2024-01-15T10:30:00.000Z' });
expect(result.success).toBe(true);
});
});
describe('addShoppingListItemSchema', () => {
it('accepts valid item', () => {
const result = addShoppingListItemSchema.safeParse({ item_name: 'Milk', quantity: 2, unit: 'cups' });
expect(result.success).toBe(true);
});
it('rejects invalid unit', () => {
const result = addShoppingListItemSchema.safeParse({ item_name: 'Milk', quantity: 2, unit: 'gallons' });
expect(result.success).toBe(false);
});
it('accepts fractional quantity', () => {
const result = addShoppingListItemSchema.safeParse({ item_name: 'Flour', quantity: 2.5, unit: 'cups' });
expect(result.success).toBe(true);
});
});
describe('updateShoppingListItemSchema', () => {
it('accepts partial update', () => {
const result = updateShoppingListItemSchema.safeParse({ checked_off: true });
expect(result.success).toBe(true);
});
it('rejects empty object', () => {
const result = updateShoppingListItemSchema.safeParse({});
expect(result.success).toBe(false);
});
});
describe('addRecipesToListSchema', () => {
it('accepts valid recipe ids', () => {
const result = addRecipesToListSchema.safeParse({
recipe_ids: ['123e4567-e89b-12d3-a456-426614174000'],
scale_factor: 2,
});
expect(result.success).toBe(true);
});
it('rejects empty recipe_ids', () => {
const result = addRecipesToListSchema.safeParse({ recipe_ids: [] });
expect(result.success).toBe(false);
});
it('rejects scale_factor > 3', () => {
const result = addRecipesToListSchema.safeParse({
recipe_ids: ['123e4567-e89b-12d3-a456-426614174000'],
scale_factor: 4,
});
expect(result.success).toBe(false);
});
it('defaults scale_factor to 1', () => {
const result = addRecipesToListSchema.safeParse({
recipe_ids: ['123e4567-e89b-12d3-a456-426614174000'],
});
expect(result.success).toBe(true);
if (result.success) expect(result.data.scale_factor).toBe(1);
});
});
describe('recipeQuerySchema', () => {
it('defaults to all filter, page 1, limit 20', () => {
const result = recipeQuerySchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.filter).toBe('all');
expect(result.data.page).toBe(1);
expect(result.data.limit).toBe(20);
}
});
it('rejects limit > 50', () => {
const result = recipeQuerySchema.safeParse({ limit: '51' });
expect(result.success).toBe(false);
});
});
describe('syncQuerySchema', () => {
it('accepts valid ISO timestamp', () => {
const result = syncQuerySchema.safeParse({ since: '2024-01-15T10:30:00.000Z' });
expect(result.success).toBe(true);
});
it('rejects invalid timestamp', () => {
const result = syncQuerySchema.safeParse({ since: 'not-a-date' });
expect(result.success).toBe(false);
});
});
});