'use strict'; const request = require('supertest'); const app = require('../../src/main/app'); const { setDb } = require('../../src/db/knex'); const { createTestDb } = require('../helpers/testDb'); const { issueToken } = require('../../src/utils/jwt'); const { v4: uuidv4 } = require('uuid'); function makeUser(overrides = {}) { return { id: uuidv4(), email: 'jane@example.com', name: 'Jane Doe', profile_picture_url: null, email_verified: true, deleted_at: null, deletion_scheduled_at: null, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), ...overrides, }; } const RECIPE_ID = '00000000-0000-0000-0000-000000000001'; function makeRecipe(overrides = {}) { return { id: RECIPE_ID, name: 'Classic Pancakes', instructions: '1. Mix. 2. Cook.', servings: 4, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), ...overrides, }; } function makeIngredients(recipeId = RECIPE_ID) { return [ { id: uuidv4(), recipe_id: recipeId, item_name: 'flour', item_name_lower: 'flour', quantity: '2.0000', unit: 'cups', }, { id: uuidv4(), recipe_id: recipeId, item_name: 'milk', item_name_lower: 'milk', quantity: '1.5000', unit: 'cups', }, { id: uuidv4(), recipe_id: recipeId, item_name: 'eggs', item_name_lower: 'eggs', quantity: '2.0000', unit: 'whole', }, ]; } describe('Recipe Routes', () => { let db; let user; let token; beforeEach(() => { user = makeUser(); db = createTestDb({ users: [user], recipes: [makeRecipe()], recipe_ingredients: makeIngredients(), pantry_items: [], }); setDb(db); token = issueToken(user).token; }); // ── GET /v1/recipes ───────────────────────────────────────────────────────── describe('GET /v1/recipes', () => { it('returns all recipes with availability', async () => { const res = await request(app) .get('/v1/recipes') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.recipes).toHaveLength(1); expect(res.body.recipes[0].name).toBe('Classic Pancakes'); expect(res.body.recipes[0].availability).toBeDefined(); expect(res.body.synced_at).toBeDefined(); }); it('shows missing_all when pantry is empty', async () => { const res = await request(app) .get('/v1/recipes') .set('Authorization', `Bearer ${token}`); expect(res.body.recipes[0].availability.status).toBe('missing_all'); expect(res.body.recipes[0].availability.available_count).toBe(0); expect(res.body.recipes[0].availability.total_count).toBe(3); }); it('shows can_make when all ingredients are in pantry', async () => { db.seedTable('pantry_items', [ { id: uuidv4(), user_id: user.id, item_name: 'flour', item_name_lower: 'flour', quantity: 5, last_modified: new Date().toISOString(), created_at: new Date().toISOString(), updated_at: new Date().toISOString() }, { id: uuidv4(), user_id: user.id, item_name: 'milk', item_name_lower: 'milk', quantity: 2, last_modified: new Date().toISOString(), created_at: new Date().toISOString(), updated_at: new Date().toISOString() }, { id: uuidv4(), user_id: user.id, item_name: 'eggs', item_name_lower: 'eggs', quantity: 6, last_modified: new Date().toISOString(), created_at: new Date().toISOString(), updated_at: new Date().toISOString() }, ]); const res = await request(app) .get('/v1/recipes') .set('Authorization', `Bearer ${token}`); expect(res.body.recipes[0].availability.status).toBe('can_make'); expect(res.body.recipes[0].availability.available_count).toBe(3); }); it('shows partial when some ingredients are in pantry', async () => { db.seedTable('pantry_items', [ { id: uuidv4(), user_id: user.id, item_name: 'flour', item_name_lower: 'flour', quantity: 5, last_modified: new Date().toISOString(), created_at: new Date().toISOString(), updated_at: new Date().toISOString() }, ]); const res = await request(app) .get('/v1/recipes') .set('Authorization', `Bearer ${token}`); expect(res.body.recipes[0].availability.status).toBe('partial'); expect(res.body.recipes[0].availability.available_count).toBe(1); expect(res.body.recipes[0].availability.missing_ingredients).toContain('milk'); }); it('filters to available only with ?filter=available', async () => { const res = await request(app) .get('/v1/recipes?filter=available') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.recipes).toHaveLength(0); // pantry is empty }); it('returns 401 without token', async () => { const res = await request(app).get('/v1/recipes'); expect(res.status).toBe(401); }); it('returns 400 for invalid scale', async () => { const res = await request(app) .get('/v1/recipes?scale=5') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(400); }); }); // ── GET /v1/recipes/:recipe_id ────────────────────────────────────────────── describe('GET /v1/recipes/:recipe_id', () => { it('returns full recipe detail', async () => { const res = await request(app) .get(`/v1/recipes/${RECIPE_ID}`) .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.recipe.name).toBe('Classic Pancakes'); expect(res.body.recipe.instructions).toBeDefined(); expect(res.body.recipe.ingredients).toHaveLength(3); expect(res.body.recipe.scaled_servings).toBe(4); }); it('scales ingredient quantities with ?scale=2', async () => { const res = await request(app) .get(`/v1/recipes/${RECIPE_ID}?scale=2`) .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.recipe.scaled_servings).toBe(8); const flour = res.body.recipe.ingredients.find((i) => i.item_name === 'flour'); expect(flour.quantity).toBe(4.0); }); it('marks in_pantry correctly', async () => { db.seedTable('pantry_items', [ { id: uuidv4(), user_id: user.id, item_name: 'flour', item_name_lower: 'flour', quantity: 5, last_modified: new Date().toISOString(), created_at: new Date().toISOString(), updated_at: new Date().toISOString() }, ]); const res = await request(app) .get(`/v1/recipes/${RECIPE_ID}`) .set('Authorization', `Bearer ${token}`); const flour = res.body.recipe.ingredients.find((i) => i.item_name === 'flour'); const milk = res.body.recipe.ingredients.find((i) => i.item_name === 'milk'); expect(flour.in_pantry).toBe(true); expect(milk.in_pantry).toBe(false); }); it('returns 404 for non-existent recipe', async () => { const res = await request(app) .get('/v1/recipes/00000000-0000-0000-0000-999999999999') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(404); expect(res.body.code).toBe('NOT_FOUND'); }); it('returns 400 for invalid scale value', async () => { const res = await request(app) .get(`/v1/recipes/${RECIPE_ID}?scale=10`) .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(400); }); }); });