localgreenchain/__tests__/lib/transport/tracker.test.ts
Claude b8d2d5be5f
Add comprehensive testing suite for transport, demand, and vertical farming systems
- 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
2025-11-22 18:47:04 +00:00

471 lines
15 KiB
TypeScript

/**
* 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',
};
}