- Set up Jest testing framework with TypeScript support - Add unit tests for TransportChain blockchain (tracker, carbon, types) - Add unit tests for DemandForecaster (forecaster, aggregation, recommendations) - Add unit tests for VerticalFarmController (controller, recipes, environment) - Add API tests for transport, demand, and vertical-farm endpoints - Add integration tests for full lifecycle workflows: - Seed-to-seed lifecycle - Demand-to-harvest flow - VF batch lifecycle
325 lines
9.6 KiB
TypeScript
325 lines
9.6 KiB
TypeScript
/**
|
|
* Transport API Tests
|
|
* Tests for transport-related API endpoints
|
|
*
|
|
* Note: These tests are designed to test API route handlers.
|
|
* They mock the underlying services and test request/response handling.
|
|
*/
|
|
|
|
import { TransportChain, getTransportChain, setTransportChain } from '../../lib/transport/tracker';
|
|
import {
|
|
SeedAcquisitionEvent,
|
|
PlantingEvent,
|
|
HarvestEvent,
|
|
TransportLocation,
|
|
} from '../../lib/transport/types';
|
|
|
|
describe('Transport API', () => {
|
|
let chain: TransportChain;
|
|
|
|
beforeEach(() => {
|
|
chain = new TransportChain(1);
|
|
setTransportChain(chain);
|
|
});
|
|
|
|
describe('POST /api/transport/seed-acquisition', () => {
|
|
it('should record seed acquisition event', () => {
|
|
const event: SeedAcquisitionEvent = {
|
|
id: 'api-seed-001',
|
|
timestamp: new Date().toISOString(),
|
|
eventType: 'seed_acquisition',
|
|
fromLocation: createLocation('seed_bank'),
|
|
toLocation: createLocation('greenhouse'),
|
|
distanceKm: 25,
|
|
durationMinutes: 45,
|
|
transportMethod: 'electric_vehicle',
|
|
carbonFootprintKg: 0,
|
|
senderId: 'seed-bank-1',
|
|
receiverId: 'grower-1',
|
|
status: 'verified',
|
|
seedBatchId: 'batch-001',
|
|
sourceType: 'seed_bank',
|
|
species: 'Solanum lycopersicum',
|
|
quantity: 100,
|
|
quantityUnit: 'seeds',
|
|
generation: 1,
|
|
};
|
|
|
|
const block = chain.recordEvent(event);
|
|
|
|
expect(block.transportEvent.id).toBe('api-seed-001');
|
|
expect(block.transportEvent.eventType).toBe('seed_acquisition');
|
|
});
|
|
|
|
it('should reject invalid seed acquisition data', () => {
|
|
const invalidEvent = {
|
|
id: 'invalid-001',
|
|
eventType: 'seed_acquisition',
|
|
// Missing required fields
|
|
};
|
|
|
|
expect(() => {
|
|
chain.recordEvent(invalidEvent as any);
|
|
}).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('POST /api/transport/planting', () => {
|
|
it('should record planting event', () => {
|
|
const event: PlantingEvent = {
|
|
id: 'api-planting-001',
|
|
timestamp: new Date().toISOString(),
|
|
eventType: 'planting',
|
|
fromLocation: createLocation('greenhouse'),
|
|
toLocation: createLocation('greenhouse'),
|
|
distanceKm: 0,
|
|
durationMinutes: 10,
|
|
transportMethod: 'walking',
|
|
carbonFootprintKg: 0,
|
|
senderId: 'grower-1',
|
|
receiverId: 'grower-1',
|
|
status: 'verified',
|
|
seedBatchId: 'batch-001',
|
|
plantIds: ['plant-001', 'plant-002'],
|
|
plantingMethod: 'indoor_start',
|
|
quantityPlanted: 2,
|
|
growingEnvironment: 'greenhouse',
|
|
};
|
|
|
|
const block = chain.recordEvent(event);
|
|
|
|
expect(block.transportEvent.eventType).toBe('planting');
|
|
expect((block.transportEvent as PlantingEvent).plantIds.length).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('POST /api/transport/harvest', () => {
|
|
it('should record harvest event', () => {
|
|
const event: HarvestEvent = {
|
|
id: 'api-harvest-001',
|
|
timestamp: new Date().toISOString(),
|
|
eventType: 'harvest',
|
|
fromLocation: createLocation('farm'),
|
|
toLocation: createLocation('warehouse'),
|
|
distanceKm: 10,
|
|
durationMinutes: 30,
|
|
transportMethod: 'electric_truck',
|
|
carbonFootprintKg: 0,
|
|
senderId: 'grower-1',
|
|
receiverId: 'distributor-1',
|
|
status: 'verified',
|
|
plantIds: ['plant-001'],
|
|
harvestBatchId: 'harvest-001',
|
|
harvestType: 'full',
|
|
produceType: 'tomatoes',
|
|
grossWeight: 10,
|
|
netWeight: 9.5,
|
|
weightUnit: 'kg',
|
|
packagingType: 'crates',
|
|
temperatureRequired: { min: 10, max: 15, optimal: 12, unit: 'celsius' },
|
|
shelfLifeHours: 168,
|
|
seedsSaved: false,
|
|
};
|
|
|
|
const block = chain.recordEvent(event);
|
|
|
|
expect(block.transportEvent.eventType).toBe('harvest');
|
|
});
|
|
});
|
|
|
|
describe('GET /api/transport/journey/[plantId]', () => {
|
|
it('should return plant journey', () => {
|
|
const plantId = 'journey-plant-001';
|
|
|
|
// Record events for plant
|
|
const plantingEvent: PlantingEvent = {
|
|
id: 'journey-planting-001',
|
|
timestamp: new Date().toISOString(),
|
|
eventType: 'planting',
|
|
fromLocation: createLocation('greenhouse'),
|
|
toLocation: createLocation('greenhouse'),
|
|
distanceKm: 0,
|
|
durationMinutes: 5,
|
|
transportMethod: 'walking',
|
|
carbonFootprintKg: 0,
|
|
senderId: 'grower-1',
|
|
receiverId: 'grower-1',
|
|
status: 'verified',
|
|
seedBatchId: 'batch-001',
|
|
plantIds: [plantId],
|
|
plantingMethod: 'indoor_start',
|
|
quantityPlanted: 1,
|
|
growingEnvironment: 'greenhouse',
|
|
};
|
|
|
|
chain.recordEvent(plantingEvent);
|
|
|
|
const journey = chain.getPlantJourney(plantId);
|
|
|
|
expect(journey).not.toBeNull();
|
|
expect(journey?.plantId).toBe(plantId);
|
|
expect(journey?.events.length).toBe(1);
|
|
});
|
|
|
|
it('should return null for unknown plant', () => {
|
|
const journey = chain.getPlantJourney('unknown-plant-id');
|
|
expect(journey).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('GET /api/transport/footprint/[userId]', () => {
|
|
it('should return environmental impact', () => {
|
|
const userId = 'footprint-user-001';
|
|
|
|
const event: SeedAcquisitionEvent = {
|
|
id: 'footprint-event-001',
|
|
timestamp: new Date().toISOString(),
|
|
eventType: 'seed_acquisition',
|
|
fromLocation: createLocation('seed_bank'),
|
|
toLocation: createLocation('greenhouse'),
|
|
distanceKm: 50,
|
|
durationMinutes: 60,
|
|
transportMethod: 'diesel_truck',
|
|
carbonFootprintKg: 0,
|
|
senderId: userId,
|
|
receiverId: userId,
|
|
status: 'verified',
|
|
seedBatchId: 'batch-footprint',
|
|
sourceType: 'seed_bank',
|
|
species: 'Test species',
|
|
quantity: 100,
|
|
quantityUnit: 'seeds',
|
|
generation: 1,
|
|
};
|
|
|
|
chain.recordEvent(event);
|
|
|
|
const impact = chain.getEnvironmentalImpact(userId);
|
|
|
|
expect(impact.totalFoodMiles).toBe(50);
|
|
expect(impact.totalCarbonKg).toBeGreaterThan(0);
|
|
expect(impact.comparisonToConventional).toBeDefined();
|
|
});
|
|
|
|
it('should return zero impact for user with no events', () => {
|
|
const impact = chain.getEnvironmentalImpact('no-events-user');
|
|
|
|
expect(impact.totalFoodMiles).toBe(0);
|
|
expect(impact.totalCarbonKg).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/transport/verify/[blockHash]', () => {
|
|
it('should verify chain integrity', () => {
|
|
chain.recordEvent(createSeedEvent());
|
|
chain.recordEvent(createSeedEvent());
|
|
|
|
const isValid = chain.isChainValid();
|
|
expect(isValid).toBe(true);
|
|
});
|
|
|
|
it('should detect tampered chain', () => {
|
|
chain.recordEvent(createSeedEvent());
|
|
|
|
// Tamper with block
|
|
chain.chain[1].transportEvent.distanceKm = 999999;
|
|
|
|
const isValid = chain.isChainValid();
|
|
expect(isValid).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/transport/qr/[id]', () => {
|
|
it('should generate QR data for plant', () => {
|
|
const plantId = 'qr-plant-001';
|
|
|
|
const plantingEvent: PlantingEvent = {
|
|
id: 'qr-planting-001',
|
|
timestamp: new Date().toISOString(),
|
|
eventType: 'planting',
|
|
fromLocation: createLocation('greenhouse'),
|
|
toLocation: createLocation('greenhouse'),
|
|
distanceKm: 0,
|
|
durationMinutes: 5,
|
|
transportMethod: 'walking',
|
|
carbonFootprintKg: 0,
|
|
senderId: 'grower-1',
|
|
receiverId: 'grower-1',
|
|
status: 'verified',
|
|
seedBatchId: 'batch-001',
|
|
plantIds: [plantId],
|
|
plantingMethod: 'indoor_start',
|
|
quantityPlanted: 1,
|
|
growingEnvironment: 'greenhouse',
|
|
};
|
|
|
|
chain.recordEvent(plantingEvent);
|
|
|
|
const qrData = chain.generateQRData(plantId, undefined);
|
|
|
|
expect(qrData.plantId).toBe(plantId);
|
|
expect(qrData.quickLookupUrl).toContain(plantId);
|
|
expect(qrData.verificationCode).toBeDefined();
|
|
});
|
|
|
|
it('should generate QR data for batch', () => {
|
|
const batchId = 'qr-batch-001';
|
|
|
|
const event = createSeedEvent();
|
|
event.seedBatchId = batchId;
|
|
chain.recordEvent(event);
|
|
|
|
const qrData = chain.generateQRData(undefined, batchId);
|
|
|
|
expect(qrData.batchId).toBe(batchId);
|
|
});
|
|
});
|
|
|
|
describe('Response Format', () => {
|
|
it('should return blocks with all required fields', () => {
|
|
const block = chain.recordEvent(createSeedEvent());
|
|
|
|
expect(block.index).toBeDefined();
|
|
expect(block.timestamp).toBeDefined();
|
|
expect(block.transportEvent).toBeDefined();
|
|
expect(block.previousHash).toBeDefined();
|
|
expect(block.hash).toBeDefined();
|
|
expect(block.nonce).toBeDefined();
|
|
expect(block.cumulativeCarbonKg).toBeDefined();
|
|
expect(block.cumulativeFoodMiles).toBeDefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
// Helper functions
|
|
function createLocation(type: string): TransportLocation {
|
|
return {
|
|
latitude: 40.7128,
|
|
longitude: -74.006,
|
|
locationType: type as any,
|
|
facilityName: `Test ${type}`,
|
|
};
|
|
}
|
|
|
|
function createSeedEvent(): SeedAcquisitionEvent {
|
|
return {
|
|
id: `seed-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
timestamp: new Date().toISOString(),
|
|
eventType: 'seed_acquisition',
|
|
fromLocation: createLocation('seed_bank'),
|
|
toLocation: createLocation('greenhouse'),
|
|
distanceKm: 25,
|
|
durationMinutes: 45,
|
|
transportMethod: 'electric_vehicle',
|
|
carbonFootprintKg: 0,
|
|
senderId: 'seed-bank-1',
|
|
receiverId: 'grower-1',
|
|
status: 'verified',
|
|
seedBatchId: `batch-${Date.now()}`,
|
|
sourceType: 'seed_bank',
|
|
species: 'Test species',
|
|
quantity: 100,
|
|
quantityUnit: 'seeds',
|
|
generation: 1,
|
|
};
|
|
}
|