/** * Vertical Farm API Tests * Tests for vertical farm-related API endpoints */ import { VerticalFarmController, getVerticalFarmController, } from '../../lib/vertical-farming/controller'; import { VerticalFarm, GrowingZone, ZoneEnvironmentReadings, } from '../../lib/vertical-farming/types'; describe('Vertical Farm API', () => { let controller: VerticalFarmController; beforeEach(() => { controller = new VerticalFarmController(); }); describe('POST /api/vertical-farm/register', () => { it('should register new farm', () => { const farm = createVerticalFarm('api-farm-001'); controller.registerFarm(farm); const retrieved = controller.getFarm('api-farm-001'); expect(retrieved).toBeDefined(); expect(retrieved?.name).toBe('Test Vertical Farm'); }); it('should allow updating farm', () => { const farm = createVerticalFarm('api-farm-001'); controller.registerFarm(farm); const updatedFarm = createVerticalFarm('api-farm-001'); updatedFarm.name = 'Updated Farm Name'; controller.registerFarm(updatedFarm); const retrieved = controller.getFarm('api-farm-001'); expect(retrieved?.name).toBe('Updated Farm Name'); }); }); describe('GET /api/vertical-farm/[farmId]', () => { it('should return farm details', () => { const farm = createVerticalFarm('api-farm-001'); controller.registerFarm(farm); const retrieved = controller.getFarm('api-farm-001'); expect(retrieved).toBeDefined(); expect(retrieved?.id).toBe('api-farm-001'); expect(retrieved?.zones.length).toBeGreaterThan(0); }); it('should return undefined for unknown farm', () => { const retrieved = controller.getFarm('unknown-farm'); expect(retrieved).toBeUndefined(); }); }); describe('GET /api/vertical-farm/[farmId]/zones', () => { it('should return farm zones', () => { const farm = createVerticalFarm('api-farm-001'); controller.registerFarm(farm); const retrieved = controller.getFarm('api-farm-001'); const zones = retrieved?.zones; expect(zones).toBeDefined(); expect(zones?.length).toBeGreaterThan(0); expect(zones?.[0].id).toBe('zone-001'); }); }); describe('POST /api/vertical-farm/[farmId]/zones', () => { it('should add zone to existing farm', () => { const farm = createVerticalFarm('api-farm-001'); farm.zones = []; // Start with no zones controller.registerFarm(farm); // Add zone by updating farm const updatedFarm = controller.getFarm('api-farm-001')!; updatedFarm.zones.push(createGrowingZone('new-zone', 'New Zone', 1)); expect(updatedFarm.zones.length).toBe(1); }); }); describe('GET /api/vertical-farm/[farmId]/analytics', () => { it('should return farm analytics', () => { const farm = createVerticalFarm('api-farm-001'); controller.registerFarm(farm); const analytics = controller.generateAnalytics('api-farm-001', 30); expect(analytics.farmId).toBe('api-farm-001'); expect(analytics.period).toBe('30 days'); }); it('should include yield metrics', () => { const farm = createVerticalFarm('api-farm-001'); controller.registerFarm(farm); // Start and complete a batch for analytics data const recipes = controller.getRecipes(); const batch = controller.startCropBatch( 'api-farm-001', 'zone-001', recipes[0].id, 'seed-batch-001', 100 ); controller.completeHarvest(batch.id, 15.0, 'A'); const analytics = controller.generateAnalytics('api-farm-001', 30); expect(analytics.totalYieldKg).toBeGreaterThan(0); }); it('should throw error for unknown farm', () => { expect(() => { controller.generateAnalytics('unknown-farm', 30); }).toThrow('Farm unknown-farm not found'); }); }); describe('POST /api/vertical-farm/batch/start', () => { it('should start crop batch', () => { const farm = createVerticalFarm('api-farm-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); const batch = controller.startCropBatch( 'api-farm-001', 'zone-001', recipes[0].id, 'seed-batch-001', 100 ); expect(batch.id).toBeDefined(); expect(batch.plantCount).toBe(100); expect(batch.status).toBe('germinating'); }); it('should validate farm exists', () => { expect(() => { controller.startCropBatch( 'unknown-farm', 'zone-001', 'recipe-001', 'seed-001', 100 ); }).toThrow('Farm unknown-farm not found'); }); it('should validate zone exists', () => { const farm = createVerticalFarm('api-farm-001'); controller.registerFarm(farm); expect(() => { controller.startCropBatch( 'api-farm-001', 'unknown-zone', 'recipe-001', 'seed-001', 100 ); }).toThrow(); }); it('should validate recipe exists', () => { const farm = createVerticalFarm('api-farm-001'); controller.registerFarm(farm); expect(() => { controller.startCropBatch( 'api-farm-001', 'zone-001', 'unknown-recipe', 'seed-001', 100 ); }).toThrow(); }); }); describe('GET /api/vertical-farm/batch/[batchId]', () => { it('should return batch details', () => { const farm = createVerticalFarm('api-farm-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); const batch = controller.startCropBatch( 'api-farm-001', 'zone-001', recipes[0].id, 'seed-batch-001', 100 ); // Update progress const updated = controller.updateBatchProgress(batch.id); expect(updated.id).toBe(batch.id); expect(updated.currentDay).toBeGreaterThanOrEqual(0); }); it('should throw error for unknown batch', () => { expect(() => { controller.updateBatchProgress('unknown-batch'); }).toThrow('Batch unknown-batch not found'); }); }); describe('PUT /api/vertical-farm/batch/[batchId]/environment', () => { it('should record environment readings', () => { const farm = createVerticalFarm('api-farm-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); controller.startCropBatch( 'api-farm-001', 'zone-001', recipes[0].id, 'seed-batch-001', 100 ); const readings: ZoneEnvironmentReadings = { timestamp: new Date().toISOString(), temperatureC: 22, humidityPercent: 70, co2Ppm: 1000, vpd: 1.0, ppfd: 300, dli: 17, waterTempC: 20, ec: 1.5, ph: 6.0, dissolvedOxygenPpm: 8, airflowMs: 0.5, alerts: [], }; const alerts = controller.recordEnvironment('zone-001', readings); expect(Array.isArray(alerts)).toBe(true); }); it('should detect environment alerts', () => { const farm = createVerticalFarm('api-farm-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); controller.startCropBatch( 'api-farm-001', 'zone-001', recipes[0].id, 'seed-batch-001', 100 ); const readings: ZoneEnvironmentReadings = { timestamp: new Date().toISOString(), temperatureC: 35, // Too high humidityPercent: 30, // Too low co2Ppm: 1000, vpd: 1.0, ppfd: 300, dli: 17, waterTempC: 20, ec: 1.5, ph: 6.0, dissolvedOxygenPpm: 8, airflowMs: 0.5, alerts: [], }; const alerts = controller.recordEnvironment('zone-001', readings); expect(alerts.length).toBeGreaterThan(0); }); }); describe('POST /api/vertical-farm/batch/[batchId]/harvest', () => { it('should complete harvest', () => { const farm = createVerticalFarm('api-farm-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); const batch = controller.startCropBatch( 'api-farm-001', 'zone-001', recipes[0].id, 'seed-batch-001', 100 ); const completed = controller.completeHarvest(batch.id, 15.5, 'A'); expect(completed.status).toBe('completed'); expect(completed.actualYieldKg).toBe(15.5); expect(completed.qualityGrade).toBe('A'); }); it('should throw error for unknown batch', () => { expect(() => { controller.completeHarvest('unknown-batch', 10, 'A'); }).toThrow('Batch unknown-batch not found'); }); }); describe('GET /api/vertical-farm/recipes', () => { it('should return all recipes', () => { const recipes = controller.getRecipes(); expect(recipes.length).toBeGreaterThan(0); expect(recipes[0].id).toBeDefined(); expect(recipes[0].stages.length).toBeGreaterThan(0); }); it('should include default recipes', () => { const recipes = controller.getRecipes(); const cropTypes = recipes.map(r => r.cropType); expect(cropTypes).toContain('lettuce'); expect(cropTypes).toContain('basil'); expect(cropTypes).toContain('microgreens'); }); }); describe('Response Formats', () => { it('should return consistent farm format', () => { const farm = createVerticalFarm('api-farm-001'); controller.registerFarm(farm); const retrieved = controller.getFarm('api-farm-001'); expect(retrieved?.id).toBeDefined(); expect(retrieved?.name).toBeDefined(); expect(retrieved?.ownerId).toBeDefined(); expect(retrieved?.location).toBeDefined(); expect(retrieved?.specs).toBeDefined(); expect(retrieved?.zones).toBeDefined(); expect(retrieved?.status).toBeDefined(); }); it('should return consistent batch format', () => { const farm = createVerticalFarm('api-farm-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); const batch = controller.startCropBatch( 'api-farm-001', 'zone-001', recipes[0].id, 'seed-batch-001', 100 ); expect(batch.id).toBeDefined(); expect(batch.farmId).toBeDefined(); expect(batch.zoneId).toBeDefined(); expect(batch.cropType).toBeDefined(); expect(batch.plantCount).toBeDefined(); expect(batch.status).toBeDefined(); expect(batch.expectedYieldKg).toBeDefined(); }); it('should return consistent analytics format', () => { const farm = createVerticalFarm('api-farm-001'); controller.registerFarm(farm); const analytics = controller.generateAnalytics('api-farm-001', 30); expect(analytics.farmId).toBeDefined(); expect(analytics.generatedAt).toBeDefined(); expect(analytics.period).toBeDefined(); expect(analytics.totalYieldKg).toBeDefined(); expect(analytics.cropCyclesCompleted).toBeDefined(); expect(analytics.topCropsByYield).toBeDefined(); }); }); }); // Helper functions function createVerticalFarm(id: string): VerticalFarm { return { id, name: 'Test Vertical Farm', ownerId: 'owner-001', location: { latitude: 40.7128, longitude: -74.006, address: '123 Farm Street', city: 'New York', country: 'USA', timezone: 'America/New_York', }, specs: { totalAreaSqm: 500, growingAreaSqm: 400, numberOfLevels: 4, ceilingHeightM: 3, totalGrowingPositions: 4000, currentActivePlants: 0, powerCapacityKw: 100, waterStorageL: 5000, backupPowerHours: 24, certifications: ['organic'], buildingType: 'warehouse', insulation: 'high_efficiency', }, zones: [createGrowingZone('zone-001', 'Zone A', 1)], environmentalControl: { hvacUnits: [], co2Injection: { type: 'tank', capacityKg: 50, currentLevelKg: 40, injectionRateKgPerHour: 2, status: 'maintaining' }, humidification: { type: 'ultrasonic', capacityLPerHour: 10, status: 'running', currentOutput: 5 }, airCirculation: { fans: [] }, controlMode: 'adaptive', }, irrigationSystem: { type: 'recirculating', freshWaterTankL: 2000, freshWaterLevelL: 1800, nutrientTankL: 500, nutrientLevelL: 450, wasteTankL: 200, wasteLevelL: 50, waterTreatment: { ro: true, uv: true, ozone: false, filtration: '10 micron' }, pumps: [], irrigationSchedule: [], }, lightingSystem: { type: 'LED', fixtures: [], lightSchedules: [], totalWattage: 5000, currentWattage: 3000, efficacyUmolJ: 2.5, }, nutrientSystem: { mixingMethod: 'fully_auto', stockSolutions: [], dosingPumps: [], currentRecipe: { id: 'default', name: 'Default', cropType: 'general', growthStage: 'vegetative', targetEc: 1.5, targetPh: 6.0, ratios: { n: 200, p: 50, k: 200, ca: 200, mg: 50, s: 100, fe: 5, mn: 0.5, zn: 0.3, cu: 0.1, b: 0.5, mo: 0.05 }, dosingRatiosMlPerL: [], }, monitoring: { ec: 1.5, ph: 6.0, lastCalibration: new Date().toISOString(), calibrationDue: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), }, }, automationLevel: 'semi_automated', automationSystems: [], status: 'operational', operationalSince: '2024-01-01', lastMaintenanceDate: new Date().toISOString(), currentCapacityUtilization: 75, averageYieldEfficiency: 85, energyEfficiencyScore: 80, }; } function createGrowingZone(id: string, name: string, level: number): GrowingZone { return { id, name, level, areaSqm: 50, lengthM: 10, widthM: 5, growingMethod: 'NFT', plantPositions: 500, currentCrop: '', plantIds: [], plantingDate: '', expectedHarvestDate: '', environmentTargets: { temperatureC: { min: 18, max: 24, target: 21 }, humidityPercent: { min: 60, max: 80, target: 70 }, co2Ppm: { min: 800, max: 1200, target: 1000 }, lightPpfd: { min: 200, max: 400, target: 300 }, lightHours: 16, nutrientEc: { min: 1.2, max: 1.8, target: 1.5 }, nutrientPh: { min: 5.8, max: 6.2, target: 6.0 }, waterTempC: { min: 18, max: 22, target: 20 }, }, currentEnvironment: { timestamp: new Date().toISOString(), temperatureC: 21, humidityPercent: 70, co2Ppm: 1000, vpd: 1.0, ppfd: 300, dli: 17, waterTempC: 20, ec: 1.5, ph: 6.0, dissolvedOxygenPpm: 8, airflowMs: 0.5, alerts: [], }, status: 'empty', }; }