describe('Oil Data Integrity', () => { it('all oils have valid prices', () => { cy.request('/api/oils').then(res => { res.body.forEach(oil => { expect(oil.name).to.be.a('string').and.not.be.empty expect(oil.bottle_price).to.be.a('number').and.be.gte(0) expect(oil.drop_count).to.be.a('number').and.be.gt(0) }) }) }) it('common oils exist in the database', () => { const expected = ['薰衣草', '茶树', '乳香', '柠檬', '椒样薄荷', '椰子油'] cy.request('/api/oils').then(res => { const names = res.body.map(o => o.name) expected.forEach(name => { expect(names).to.include(name) }) }) }) it('oil price per drop is correctly calculated', () => { cy.request('/api/oils').then(res => { res.body.forEach(oil => { const ppd = oil.bottle_price / oil.drop_count expect(ppd).to.be.a('number') expect(ppd).to.be.gte(0) expect(ppd).to.be.lte(100) // sanity check: no oil costs >100 per drop }) }) }) it('drop counts match known volume standards', () => { // Standard doTERRA volumes: 5ml=93, 10ml=186, 15ml=280 const validDropCounts = [46, 93, 160, 186, 280, 2146] cy.request('/api/oils').then(res => { const counts = new Set(res.body.map(o => o.drop_count)) // At least some should match standard volumes const matching = [...counts].filter(c => validDropCounts.includes(c)) expect(matching.length).to.be.gte(1) }) }) }) describe('Recipe Data Integrity', () => { it('all recipes have valid structure', () => { cy.request('/api/recipes').then(res => { expect(res.body.length).to.be.gte(1) res.body.forEach(recipe => { expect(recipe).to.have.property('id') expect(recipe).to.have.property('name').and.not.be.empty expect(recipe).to.have.property('ingredients').and.be.an('array') recipe.ingredients.forEach(ing => { expect(ing).to.have.property('oil_name').and.not.be.empty expect(ing).to.have.property('drops').and.be.a('number').and.be.gt(0) }) }) }) }) it('recipes reference existing oils', () => { cy.request('/api/oils').then(oilRes => { const oilNames = new Set(oilRes.body.map(o => o.name)) cy.request('/api/recipes').then(recipeRes => { let totalMissing = 0 recipeRes.body.forEach(recipe => { recipe.ingredients.forEach(ing => { if (!oilNames.has(ing.oil_name)) totalMissing++ }) }) // Allow some missing (discontinued oils), but not too many const totalIngs = recipeRes.body.reduce((s, r) => s + r.ingredients.length, 0) const missingRate = totalMissing / totalIngs expect(missingRate).to.be.lt(0.2) // less than 20% missing }) }) }) it('no duplicate recipe names', () => { cy.request('/api/recipes').then(res => { const names = res.body.map(r => r.name) const unique = new Set(names) // Allow some duplicates but flag if many const dupRate = (names.length - unique.size) / names.length expect(dupRate).to.be.lt(0.1) // less than 10% duplicates }) }) it('recipe costs are calculable', () => { cy.request('/api/oils').then(oilRes => { const oilPrices = {} oilRes.body.forEach(o => { oilPrices[o.name] = o.bottle_price / o.drop_count }) cy.request('/api/recipes').then(recipeRes => { recipeRes.body.slice(0, 20).forEach(recipe => { let cost = 0 recipe.ingredients.forEach(ing => { cost += (oilPrices[ing.oil_name] || 0) * ing.drops }) expect(cost).to.be.a('number') expect(cost).to.be.gte(0) }) }) }) }) })