Skip to content

Commit 259ecf6

Browse files
committed
feat: Add temporal validation to GeoJSON validator (Phase 3)
Implements ISO 8601 timestamp and interval validation for CSAPI temporal properties as specified in Issue camptocamp#70. - Add validateTimestamp() for ISO 8601 timestamp format checking (UTC, offset, date-only) - Add validateTemporalInterval() for interval validation with open-ended support - Add validateTemporal() dispatcher for timestamps/intervals - Properly detect impossible dates (Feb 30, Month 13) by comparing parsed date components - Integrate temporal validation into System, Deployment, Procedure, Datastream, and SamplingFeature validators - Add 22 comprehensive temporal validation tests covering valid/invalid timestamps and intervals Closes Sam-Bolling/CSAPI-Live-Testing#70
1 parent 8b90b87 commit 259ecf6

2 files changed

Lines changed: 499 additions & 3 deletions

File tree

src/ogc-api/csapi/validation/geojson-validator.spec.ts

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2282,8 +2282,353 @@ describe('GeoJSON Validators', () => {
22822282
});
22832283
});
22842284
});
2285+
2286+
// ========== TEMPORAL VALIDATION TESTS ==========
2287+
2288+
describe('Temporal Validation', () => {
2289+
describe('ISO 8601 timestamp validation', () => {
2290+
it('should accept valid timestamp with UTC timezone (Z)', () => {
2291+
const feature = {
2292+
type: 'Feature',
2293+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2294+
properties: {
2295+
featureType: 'System',
2296+
uid: 'urn:test:1',
2297+
validTime: '2024-01-15T10:30:00Z'
2298+
}
2299+
};
2300+
2301+
const result = validateSystemFeature(feature);
2302+
expect(result.valid).toBe(true);
2303+
});
2304+
2305+
it('should accept valid timestamp with offset timezone', () => {
2306+
const feature = {
2307+
type: 'Feature',
2308+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2309+
properties: {
2310+
featureType: 'System',
2311+
uid: 'urn:test:1',
2312+
validTime: '2024-01-15T10:30:00+05:30'
2313+
}
2314+
};
2315+
2316+
const result = validateSystemFeature(feature);
2317+
expect(result.valid).toBe(true);
2318+
});
2319+
2320+
it('should accept valid date-only format (YYYY-MM-DD)', () => {
2321+
const feature = {
2322+
type: 'Feature',
2323+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2324+
properties: {
2325+
featureType: 'System',
2326+
uid: 'urn:test:1',
2327+
validTime: '2024-01-15'
2328+
}
2329+
};
2330+
2331+
const result = validateSystemFeature(feature);
2332+
expect(result.valid).toBe(true);
2333+
});
2334+
2335+
it('should accept valid timestamp with milliseconds', () => {
2336+
const feature = {
2337+
type: 'Feature',
2338+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2339+
properties: {
2340+
featureType: 'System',
2341+
uid: 'urn:test:1',
2342+
validTime: '2024-01-15T10:30:00.123Z'
2343+
}
2344+
};
2345+
2346+
const result = validateSystemFeature(feature);
2347+
expect(result.valid).toBe(true);
2348+
});
2349+
2350+
it('should reject non-ISO 8601 format (US date format)', () => {
2351+
const feature = {
2352+
type: 'Feature',
2353+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2354+
properties: {
2355+
featureType: 'System',
2356+
uid: 'urn:test:1',
2357+
validTime: '01/15/2024 10:30 AM'
2358+
}
2359+
};
2360+
2361+
const result = validateSystemFeature(feature);
2362+
expect(result.valid).toBe(false);
2363+
expect(result.errors?.some(e => e.includes('Invalid ISO 8601 timestamp format'))).toBe(true);
2364+
});
2365+
2366+
it('should reject impossible date (Feb 30)', () => {
2367+
const feature = {
2368+
type: 'Feature',
2369+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2370+
properties: {
2371+
featureType: 'System',
2372+
uid: 'urn:test:1',
2373+
validTime: '2024-02-30T00:00:00Z'
2374+
}
2375+
};
2376+
2377+
const result = validateSystemFeature(feature);
2378+
expect(result.valid).toBe(false);
2379+
expect(result.errors?.some(e => e.includes('impossible date'))).toBe(true);
2380+
});
2381+
2382+
it('should reject impossible date (Month 13)', () => {
2383+
const feature = {
2384+
type: 'Feature',
2385+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2386+
properties: {
2387+
featureType: 'System',
2388+
uid: 'urn:test:1',
2389+
validTime: '2024-13-01T00:00:00Z'
2390+
}
2391+
};
2392+
2393+
const result = validateSystemFeature(feature);
2394+
expect(result.valid).toBe(false);
2395+
expect(result.errors?.some(e => e.includes('impossible date'))).toBe(true);
2396+
});
2397+
2398+
it('should warn about missing timezone', () => {
2399+
const feature = {
2400+
type: 'Feature',
2401+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2402+
properties: {
2403+
featureType: 'System',
2404+
uid: 'urn:test:1',
2405+
validTime: '2024-01-15T10:30:00'
2406+
}
2407+
};
2408+
2409+
const result = validateSystemFeature(feature);
2410+
expect(result.valid).toBe(false);
2411+
expect(result.errors?.some(e => e.includes('missing timezone'))).toBe(true);
2412+
});
2413+
2414+
it('should reject empty timestamp', () => {
2415+
const feature = {
2416+
type: 'Feature',
2417+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2418+
properties: {
2419+
featureType: 'System',
2420+
uid: 'urn:test:1',
2421+
validTime: ''
2422+
}
2423+
};
2424+
2425+
const result = validateSystemFeature(feature);
2426+
expect(result.valid).toBe(false);
2427+
expect(result.errors?.some(e => e.includes('cannot be empty'))).toBe(true);
2428+
});
2429+
});
2430+
2431+
describe('ISO 8601 interval validation', () => {
2432+
it('should accept valid closed interval (start/end)', () => {
2433+
const feature = {
2434+
type: 'Feature',
2435+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2436+
properties: {
2437+
featureType: 'System',
2438+
uid: 'urn:test:1',
2439+
validTime: '2024-01-15T00:00:00Z/2024-01-20T23:59:59Z'
2440+
}
2441+
};
2442+
2443+
const result = validateSystemFeature(feature);
2444+
expect(result.valid).toBe(true);
2445+
});
2446+
2447+
it('should accept valid open-ended start interval (../end)', () => {
2448+
const feature = {
2449+
type: 'Feature',
2450+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2451+
properties: {
2452+
featureType: 'System',
2453+
uid: 'urn:test:1',
2454+
validTime: '../2024-01-20T23:59:59Z'
2455+
}
2456+
};
2457+
2458+
const result = validateSystemFeature(feature);
2459+
expect(result.valid).toBe(true);
2460+
});
2461+
2462+
it('should accept valid open-ended end interval (start/..)', () => {
2463+
const feature = {
2464+
type: 'Feature',
2465+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2466+
properties: {
2467+
featureType: 'System',
2468+
uid: 'urn:test:1',
2469+
validTime: '2024-01-15T00:00:00Z/..'
2470+
}
2471+
};
2472+
2473+
const result = validateSystemFeature(feature);
2474+
expect(result.valid).toBe(true);
2475+
});
2476+
2477+
it('should reject reversed interval (end < start)', () => {
2478+
const feature = {
2479+
type: 'Feature',
2480+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2481+
properties: {
2482+
featureType: 'System',
2483+
uid: 'urn:test:1',
2484+
validTime: '2024-01-20T00:00:00Z/2024-01-10T00:00:00Z'
2485+
}
2486+
};
2487+
2488+
const result = validateSystemFeature(feature);
2489+
expect(result.valid).toBe(false);
2490+
expect(result.errors?.some(e => e.includes('Start time') && e.includes('must be before end time'))).toBe(true);
2491+
});
2492+
2493+
it('should reject malformed interval (missing end)', () => {
2494+
const feature = {
2495+
type: 'Feature',
2496+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2497+
properties: {
2498+
featureType: 'System',
2499+
uid: 'urn:test:1',
2500+
validTime: '2024-01-15T00:00:00Z/'
2501+
}
2502+
};
2503+
2504+
const result = validateSystemFeature(feature);
2505+
expect(result.valid).toBe(false);
2506+
expect(result.errors?.some(e => e.includes('Timestamp cannot be empty'))).toBe(true);
2507+
});
2508+
2509+
it('should reject interval with invalid start timestamp', () => {
2510+
const feature = {
2511+
type: 'Feature',
2512+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2513+
properties: {
2514+
featureType: 'System',
2515+
uid: 'urn:test:1',
2516+
validTime: '2024-02-30T00:00:00Z/2024-03-15T00:00:00Z'
2517+
}
2518+
};
2519+
2520+
const result = validateSystemFeature(feature);
2521+
expect(result.valid).toBe(false);
2522+
expect(result.errors?.some(e => e.includes('(start)') && e.includes('impossible date'))).toBe(true);
2523+
});
2524+
2525+
it('should reject interval with invalid end timestamp', () => {
2526+
const feature = {
2527+
type: 'Feature',
2528+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2529+
properties: {
2530+
featureType: 'System',
2531+
uid: 'urn:test:1',
2532+
validTime: '2024-01-15T00:00:00Z/2024-13-01T00:00:00Z'
2533+
}
2534+
};
2535+
2536+
const result = validateSystemFeature(feature);
2537+
expect(result.valid).toBe(false);
2538+
expect(result.errors?.some(e => e.includes('(end)') && e.includes('impossible date'))).toBe(true);
2539+
});
2540+
});
2541+
2542+
describe('Integration with multiple feature types', () => {
2543+
it('should validate validTime in Deployment features', () => {
2544+
const feature = {
2545+
type: 'Feature',
2546+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2547+
properties: {
2548+
featureType: 'Deployment',
2549+
uid: 'urn:test:1',
2550+
system: { href: 'http://example.org/systems/1' },
2551+
validTime: '01/15/2024 10:30 AM' // Invalid format
2552+
}
2553+
};
2554+
2555+
const result = validateDeploymentFeature(feature);
2556+
expect(result.valid).toBe(false);
2557+
expect(result.errors?.some(e => e.includes('validTime') && e.includes('Invalid ISO 8601'))).toBe(true);
2558+
});
2559+
2560+
it('should validate validTime in Procedure features', () => {
2561+
const feature = {
2562+
type: 'Feature',
2563+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2564+
properties: {
2565+
featureType: 'Procedure',
2566+
uid: 'urn:test:1',
2567+
validTime: '2024-02-30T00:00:00Z' // Impossible date
2568+
}
2569+
};
2570+
2571+
const result = validateProcedureFeature(feature);
2572+
expect(result.valid).toBe(false);
2573+
expect(result.errors?.some(e => e.includes('validTime') && e.includes('impossible date'))).toBe(true);
2574+
});
2575+
2576+
it('should validate phenomenonTime in Datastream features', () => {
2577+
const feature = {
2578+
type: 'Feature',
2579+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2580+
properties: {
2581+
featureType: 'Datastream',
2582+
uid: 'urn:test:1',
2583+
system: { href: 'http://example.org/systems/1' },
2584+
observedProperty: { href: 'http://example.org/props/temp' },
2585+
phenomenonTime: '2024-01-15T10:30:00' // Missing timezone
2586+
}
2587+
};
2588+
2589+
const result = validateDatastreamFeature(feature);
2590+
expect(result.valid).toBe(false);
2591+
expect(result.errors?.some(e => e.includes('phenomenonTime') && e.includes('missing timezone'))).toBe(true);
2592+
});
2593+
2594+
it('should validate resultTime in Datastream features', () => {
2595+
const feature = {
2596+
type: 'Feature',
2597+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2598+
properties: {
2599+
featureType: 'Datastream',
2600+
uid: 'urn:test:1',
2601+
system: { href: 'http://example.org/systems/1' },
2602+
observedProperty: { href: 'http://example.org/props/temp' },
2603+
resultTime: '2024-01-20T00:00:00Z/2024-01-10T00:00:00Z' // Reversed interval
2604+
}
2605+
};
2606+
2607+
const result = validateDatastreamFeature(feature);
2608+
expect(result.valid).toBe(false);
2609+
expect(result.errors?.some(e => e.includes('resultTime') && e.includes('must be before end time'))).toBe(true);
2610+
});
2611+
2612+
it('should validate samplingTime in SamplingFeature', () => {
2613+
const feature = {
2614+
type: 'Feature',
2615+
geometry: { type: 'Point', coordinates: [5.0, 45.0] },
2616+
properties: {
2617+
featureType: 'SamplingFeature',
2618+
uid: 'urn:test:1',
2619+
samplingTime: 'not-a-timestamp'
2620+
}
2621+
};
2622+
2623+
const result = validateSamplingFeature(feature);
2624+
expect(result.valid).toBe(false);
2625+
expect(result.errors?.some(e => e.includes('samplingTime') && e.includes('Invalid ISO 8601'))).toBe(true);
2626+
});
2627+
});
2628+
});
22852629
});
22862630

22872631

22882632

22892633

2634+

0 commit comments

Comments
 (0)