/** * TransportChain Tests * Tests for the transport tracking blockchain implementation */ import { TransportChain, getTransportChain, setTransportChain, } from '../../../lib/transport/tracker'; import { SeedAcquisitionEvent, PlantingEvent, GrowingTransportEvent, HarvestEvent, SeedSavingEvent, TransportLocation, CARBON_FACTORS, } from '../../../lib/transport/types'; describe('TransportChain', () => { let chain: TransportChain; beforeEach(() => { chain = new TransportChain(2); // Lower difficulty for faster tests }); describe('Initialization', () => { it('should create genesis block on initialization', () => { expect(chain.chain.length).toBe(1); expect(chain.chain[0].index).toBe(0); expect(chain.chain[0].previousHash).toBe('0'); expect(chain.chain[0].transportEvent.eventType).toBe('seed_acquisition'); }); it('should set default difficulty', () => { const defaultChain = new TransportChain(); expect(defaultChain.difficulty).toBe(3); }); it('should allow custom difficulty', () => { expect(chain.difficulty).toBe(2); }); }); describe('Recording Events', () => { it('should record seed acquisition event', () => { const event: SeedAcquisitionEvent = createSeedAcquisitionEvent(); const block = chain.recordEvent(event); expect(block.index).toBe(1); expect(block.transportEvent.eventType).toBe('seed_acquisition'); expect(chain.chain.length).toBe(2); }); it('should record planting event', () => { const seedEvent = createSeedAcquisitionEvent(); chain.recordEvent(seedEvent); const plantingEvent: PlantingEvent = createPlantingEvent(); const block = chain.recordEvent(plantingEvent); expect(block.transportEvent.eventType).toBe('planting'); expect((block.transportEvent as PlantingEvent).plantIds.length).toBe(2); }); it('should record growing transport event', () => { const seedEvent = createSeedAcquisitionEvent(); chain.recordEvent(seedEvent); const plantingEvent = createPlantingEvent(); chain.recordEvent(plantingEvent); const growingEvent: GrowingTransportEvent = { id: 'growing-001', timestamp: new Date().toISOString(), eventType: 'growing_transport', fromLocation: createLocation('greenhouse'), toLocation: createLocation('farm'), distanceKm: 5, durationMinutes: 30, transportMethod: 'electric_vehicle', carbonFootprintKg: 0, senderId: 'grower-1', receiverId: 'grower-1', status: 'verified', plantIds: ['plant-001', 'plant-002'], reason: 'transplant', plantStage: 'seedling', handlingMethod: 'potted', rootDisturbance: 'minimal', acclimatizationRequired: true, acclimatizationDays: 3, }; const block = chain.recordEvent(growingEvent); expect(block.transportEvent.eventType).toBe('growing_transport'); }); it('should record harvest event', () => { const harvestEvent: HarvestEvent = { id: 'harvest-001', timestamp: new Date().toISOString(), eventType: 'harvest', fromLocation: createLocation('farm'), toLocation: createLocation('warehouse'), distanceKm: 10, durationMinutes: 45, transportMethod: 'electric_truck', carbonFootprintKg: 0, senderId: 'grower-1', receiverId: 'distributor-1', status: 'verified', plantIds: ['plant-001', 'plant-002'], harvestBatchId: 'harvest-batch-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(harvestEvent); expect(block.transportEvent.eventType).toBe('harvest'); expect((block.transportEvent as HarvestEvent).netWeight).toBe(9.5); }); it('should accumulate carbon footprint across blocks', () => { const event1 = createSeedAcquisitionEvent(); event1.distanceKm = 10; event1.transportMethod = 'gasoline_vehicle'; const block1 = chain.recordEvent(event1); const event2 = createSeedAcquisitionEvent(); event2.id = 'seed-002'; event2.distanceKm = 20; event2.transportMethod = 'diesel_truck'; const block2 = chain.recordEvent(event2); expect(block2.cumulativeCarbonKg).toBeGreaterThan(block1.cumulativeCarbonKg); }); it('should accumulate food miles across blocks', () => { const event1 = createSeedAcquisitionEvent(); event1.distanceKm = 10; const block1 = chain.recordEvent(event1); const event2 = createSeedAcquisitionEvent(); event2.id = 'seed-002'; event2.distanceKm = 20; const block2 = chain.recordEvent(event2); expect(block2.cumulativeFoodMiles).toBe( block1.cumulativeFoodMiles + 20 ); }); }); describe('Chain Integrity', () => { it('should verify chain integrity', () => { chain.recordEvent(createSeedAcquisitionEvent()); chain.recordEvent(createPlantingEvent()); expect(chain.isChainValid()).toBe(true); }); it('should detect tampered blocks', () => { chain.recordEvent(createSeedAcquisitionEvent()); // Tamper with the chain chain.chain[1].transportEvent.distanceKm = 999; expect(chain.isChainValid()).toBe(false); }); it('should detect broken chain links', () => { chain.recordEvent(createSeedAcquisitionEvent()); chain.recordEvent(createPlantingEvent()); // Break the chain link chain.chain[2].previousHash = 'fake-hash'; expect(chain.isChainValid()).toBe(false); }); it('should maintain proper hash linking', () => { chain.recordEvent(createSeedAcquisitionEvent()); chain.recordEvent(createPlantingEvent()); for (let i = 1; i < chain.chain.length; i++) { expect(chain.chain[i].previousHash).toBe(chain.chain[i - 1].hash); } }); }); describe('Plant Journey', () => { it('should track plant journey across events', () => { const plantId = 'plant-001'; // Record planting const plantingEvent = createPlantingEvent(); plantingEvent.plantIds = [plantId]; chain.recordEvent(plantingEvent); // Record growing transport const growingEvent: GrowingTransportEvent = { id: 'growing-001', timestamp: new Date().toISOString(), eventType: 'growing_transport', fromLocation: createLocation('greenhouse'), toLocation: createLocation('farm'), distanceKm: 5, durationMinutes: 30, transportMethod: 'walking', carbonFootprintKg: 0, senderId: 'grower-1', receiverId: 'grower-1', status: 'verified', plantIds: [plantId], reason: 'transplant', plantStage: 'vegetative', handlingMethod: 'potted', rootDisturbance: 'minimal', acclimatizationRequired: false, }; chain.recordEvent(growingEvent); const journey = chain.getPlantJourney(plantId); expect(journey).not.toBeNull(); expect(journey?.plantId).toBe(plantId); expect(journey?.events.length).toBe(2); expect(journey?.totalFoodMiles).toBeGreaterThan(0); }); it('should return null for unknown plant', () => { const journey = chain.getPlantJourney('unknown-plant'); expect(journey).toBeNull(); }); it('should calculate correct journey metrics', () => { const plantId = 'plant-journey-test'; const plantingEvent = createPlantingEvent(); plantingEvent.plantIds = [plantId]; plantingEvent.distanceKm = 10; plantingEvent.durationMinutes = 60; chain.recordEvent(plantingEvent); const journey = chain.getPlantJourney(plantId); expect(journey?.totalFoodMiles).toBe(10); expect(journey?.daysInTransit).toBe(0); // 60 min = ~0 days }); it('should track current stage correctly', () => { const plantId = 'stage-test-plant'; const plantingEvent = createPlantingEvent(); plantingEvent.plantIds = [plantId]; chain.recordEvent(plantingEvent); let journey = chain.getPlantJourney(plantId); expect(journey?.currentStage).toBe('seedling'); const growingEvent: GrowingTransportEvent = { id: 'growing-stage', timestamp: new Date().toISOString(), eventType: 'growing_transport', fromLocation: createLocation('greenhouse'), toLocation: createLocation('farm'), distanceKm: 1, durationMinutes: 10, transportMethod: 'walking', carbonFootprintKg: 0, senderId: 'grower-1', receiverId: 'grower-1', status: 'verified', plantIds: [plantId], reason: 'relocation', plantStage: 'flowering', handlingMethod: 'potted', rootDisturbance: 'none', acclimatizationRequired: false, }; chain.recordEvent(growingEvent); journey = chain.getPlantJourney(plantId); expect(journey?.currentStage).toBe('flowering'); }); }); describe('Environmental Impact', () => { it('should calculate environmental impact for user', () => { const userId = 'user-impact-test'; const event = createSeedAcquisitionEvent(); event.senderId = userId; event.distanceKm = 50; event.transportMethod = 'diesel_truck'; chain.recordEvent(event); const impact = chain.getEnvironmentalImpact(userId); expect(impact.totalFoodMiles).toBe(50); expect(impact.totalCarbonKg).toBeGreaterThan(0); expect(impact.breakdownByMethod['diesel_truck']).toBeDefined(); expect(impact.breakdownByEventType['seed_acquisition']).toBeDefined(); }); it('should compare to conventional agriculture', () => { const userId = 'compare-test'; const event = createSeedAcquisitionEvent(); event.senderId = userId; event.distanceKm = 10; // Very short distance event.transportMethod = 'bicycle'; // Zero carbon chain.recordEvent(event); const impact = chain.getEnvironmentalImpact(userId); expect(impact.comparisonToConventional.milesSaved).toBeGreaterThan(0); }); it('should return zero impact for user with no events', () => { const impact = chain.getEnvironmentalImpact('nonexistent-user'); expect(impact.totalCarbonKg).toBe(0); expect(impact.totalFoodMiles).toBe(0); }); }); describe('QR Code Generation', () => { it('should generate valid QR data for plant', () => { const plantId = 'qr-test-plant'; const plantingEvent = createPlantingEvent(); plantingEvent.plantIds = [plantId]; chain.recordEvent(plantingEvent); const qrData = chain.generateQRData(plantId, undefined); expect(qrData.plantId).toBe(plantId); expect(qrData.quickLookupUrl).toContain(plantId); expect(qrData.lineageHash).toBeDefined(); expect(qrData.verificationCode).toMatch(/^[A-F0-9]{8}$/); }); it('should generate QR data for batch', () => { const batchId = 'seed-batch-001'; const event = createSeedAcquisitionEvent(); event.seedBatchId = batchId; chain.recordEvent(event); const qrData = chain.generateQRData(undefined, batchId); expect(qrData.batchId).toBe(batchId); expect(qrData.lastEventType).toBe('seed_acquisition'); }); it('should include blockchain address', () => { const qrData = chain.generateQRData('any-id', undefined); expect(qrData.blockchainAddress).toHaveLength(42); }); }); describe('Serialization', () => { it('should export to JSON', () => { chain.recordEvent(createSeedAcquisitionEvent()); const json = chain.toJSON(); expect(json).toHaveProperty('difficulty'); expect(json).toHaveProperty('chain'); expect((json as any).chain.length).toBe(2); }); it('should import from JSON', () => { chain.recordEvent(createSeedAcquisitionEvent()); chain.recordEvent(createPlantingEvent()); const json = chain.toJSON(); const restored = TransportChain.fromJSON(json); expect(restored.chain.length).toBe(chain.chain.length); expect(restored.isChainValid()).toBe(true); }); it('should rebuild indexes after import', () => { const plantId = 'import-test-plant'; const plantingEvent = createPlantingEvent(); plantingEvent.plantIds = [plantId]; chain.recordEvent(plantingEvent); const json = chain.toJSON(); const restored = TransportChain.fromJSON(json); const journey = restored.getPlantJourney(plantId); expect(journey).not.toBeNull(); }); }); describe('Singleton', () => { it('should return same instance from getTransportChain', () => { const chain1 = getTransportChain(); const chain2 = getTransportChain(); expect(chain1).toBe(chain2); }); it('should allow setting custom chain', () => { const customChain = new TransportChain(1); setTransportChain(customChain); expect(getTransportChain()).toBe(customChain); }); }); }); // Helper functions function createLocation(type: string): TransportLocation { return { latitude: 40.7128 + Math.random() * 0.1, longitude: -74.006 + Math.random() * 0.1, locationType: type as any, facilityName: `Test ${type}`, }; } function createSeedAcquisitionEvent(): SeedAcquisitionEvent { return { id: `seed-${Date.now()}`, 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: 'seed-batch-001', sourceType: 'seed_bank', species: 'Solanum lycopersicum', variety: 'Roma', quantity: 100, quantityUnit: 'seeds', generation: 1, germinationRate: 95, certifications: ['organic', 'heirloom'], }; } function createPlantingEvent(): PlantingEvent { return { id: `planting-${Date.now()}`, timestamp: new Date().toISOString(), eventType: 'planting', fromLocation: createLocation('greenhouse'), toLocation: createLocation('greenhouse'), distanceKm: 0.01, durationMinutes: 5, transportMethod: 'walking', carbonFootprintKg: 0, senderId: 'grower-1', receiverId: 'grower-1', status: 'verified', seedBatchId: 'seed-batch-001', plantIds: ['plant-001', 'plant-002'], plantingMethod: 'indoor_start', quantityPlanted: 2, growingEnvironment: 'greenhouse', }; }