diff --git a/app/page.tsx b/app/page.tsx index 8649186..fc6cfa8 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -10,6 +10,7 @@ import AircraftForm from "../components/AircraftForm"; import LegalDisclaimerModal from "../components/LegalDisclaimerModal"; import LandingPage from "../components/LandingPage"; import { isPointInPolygon, getCGLimitsAtWeight } from "../utils/calculations"; +import { validateImportedFleet } from "../utils/validation"; export interface CustomStation { id: string; @@ -48,7 +49,15 @@ export default function Home() { useEffect(() => { const loadedFleet = localStorage.getItem("wb_saved_fleet"); if (loadedFleet) { - try { setSavedPlanes(JSON.parse(loadedFleet)); } catch (e) { console.error(e); } + try { + const parsed = JSON.parse(loadedFleet); + const validated = validateImportedFleet(parsed); + if (validated) { + setSavedPlanes(validated); + } else { + console.error("Invalid fleet data in localStorage"); + } + } catch (e) { console.error(e); } } const savedTheme = localStorage.getItem("wb_theme"); if (savedTheme === "dark") { @@ -100,13 +109,14 @@ export default function Home() { reader.onload = (event) => { try { const json = JSON.parse(event.target?.result as string); - if (Array.isArray(json)) { - setSavedPlanes(json); - localStorage.setItem("wb_saved_fleet", JSON.stringify(json)); + const validated = validateImportedFleet(json); + if (validated) { + setSavedPlanes(validated); + localStorage.setItem("wb_saved_fleet", JSON.stringify(validated)); alert("Fleet imported successfully!"); setShowSettings(false); } else { - alert("Invalid file format."); + alert("Invalid file format. Please upload a valid fleet JSON file."); } } catch (err) { alert("Error parsing file."); diff --git a/public/sw.js b/public/sw.js index 88e24e0..98085b6 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1 +1 @@ -if(!self.define){let e,s={};const a=(a,n)=>(a=new URL(a+".js",n).href,s[a]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=a,e.onload=s,document.head.appendChild(e)}else e=a,importScripts(a),s()}).then(()=>{let e=s[a];if(!e)throw new Error(`Module ${a} didn’t register its module`);return e}));self.define=(n,i)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let c={};const r=e=>a(e,t),d={module:{uri:t},exports:c,require:r};s[t]=Promise.all(n.map(e=>d[e]||r(e))).then(e=>(i(...e),c))}}define(["./workbox-f1770938"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/_Qk99KjPP2iEqDRtSZTeY/_buildManifest.js",revision:"745555c29d619878e260d8023b7014c1"},{url:"/_next/static/_Qk99KjPP2iEqDRtSZTeY/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/4bd1b696-deba172d32c79f82.js",revision:"deba172d32c79f82"},{url:"/_next/static/chunks/928-34428627dfa9fa40.js",revision:"34428627dfa9fa40"},{url:"/_next/static/chunks/973-e432f2a884511a8e.js",revision:"e432f2a884511a8e"},{url:"/_next/static/chunks/app/_global-error/page-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/app/_not-found/page-f7e9e5d73062cbe5.js",revision:"f7e9e5d73062cbe5"},{url:"/_next/static/chunks/app/layout-83a3f45ebc4f840c.js",revision:"83a3f45ebc4f840c"},{url:"/_next/static/chunks/app/legal/page-fe212f5a92cb34b9.js",revision:"fe212f5a92cb34b9"},{url:"/_next/static/chunks/app/page-892516dfce2a19f4.js",revision:"892516dfce2a19f4"},{url:"/_next/static/chunks/framework-4e51298db41fcfd4.js",revision:"4e51298db41fcfd4"},{url:"/_next/static/chunks/main-2682d92e5634a569.js",revision:"2682d92e5634a569"},{url:"/_next/static/chunks/main-app-abcb25e568d7cd6e.js",revision:"abcb25e568d7cd6e"},{url:"/_next/static/chunks/next/dist/client/components/builtin/app-error-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/next/dist/client/components/builtin/forbidden-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/next/dist/client/components/builtin/global-error-1dc6a36b040319f2.js",revision:"1dc6a36b040319f2"},{url:"/_next/static/chunks/next/dist/client/components/builtin/not-found-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/next/dist/client/components/builtin/unauthorized-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-07bf70a7cee35489.js",revision:"07bf70a7cee35489"},{url:"/_next/static/css/7857fc8dde1f0206.css",revision:"7857fc8dde1f0206"},{url:"/_next/static/media/19cfc7226ec3afaa-s.woff2",revision:"9dda5cfc9a46f256d0e131bb535e46f8"},{url:"/_next/static/media/21350d82a1f187e9-s.woff2",revision:"4e2553027f1d60eff32898367dd4d541"},{url:"/_next/static/media/8e9860b6e62d6359-s.woff2",revision:"01ba6c2a184b8cba08b0d57167664d75"},{url:"/_next/static/media/ba9851c3c22cd980-s.woff2",revision:"9e494903d6b0ffec1a1e14d34427d44d"},{url:"/_next/static/media/c5fe6dc8356a8c31-s.woff2",revision:"027a89e9ab733a145db70f09b8a18b42"},{url:"/_next/static/media/df0a9ae256c0569c-s.woff2",revision:"d54db44de5ccb18886ece2fda72bdfe0"},{url:"/_next/static/media/e4af272ccee01ff0-s.p.woff2",revision:"65850a373e258f1c897a2b3d75eb74de"},{url:"/apple-touch-icon.png",revision:"b73ace5d08ca50ae4d207ae49aa324a7"},{url:"/assets/screenshot-calculator.png",revision:"b5fb22478199d5d0a0567d6c3bd2c05d"},{url:"/assets/screenshot-hangar.png",revision:"ebf24315ea400345ca95724c2f38067f"},{url:"/assets/screenshot-report.png",revision:"153a01d7958719252c31e1a52830eb0f"},{url:"/file.svg",revision:"d09f95206c3fa0bb9bd9fefabfd0ea71"},{url:"/globe.svg",revision:"2aaafa6a49b6563925fe440891e32717"},{url:"/icon-192.png",revision:"4cc4ba11db593a4f3899fc2c628e4bfb"},{url:"/icon-512.png",revision:"808fb568cffbb81a989f7e187df3fcc9"},{url:"/manifest.json",revision:"4b7e0c73a2888962193970e2f5bd7149"},{url:"/next.svg",revision:"8e061864f388b47f33a1c3780831193e"},{url:"/swe-worker-5c72df51bb1f6ee0.js",revision:"76fdd3369f623a3edcf74ce2200bfdd0"},{url:"/vercel.svg",revision:"c0af2f507b369b085b35ef4bbe3bcf1e"},{url:"/window.svg",revision:"a2760511c65806022ad20adf74370ff3"}],{ignoreURLParametersMatching:[/^utm_/,/^fbclid$/]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({response:e})=>e&&"opaqueredirect"===e.type?new Response(e.body,{status:200,statusText:"OK",headers:e.headers}):e}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:2592e3})]}),"GET"),e.registerRoute(/\/_next\/static.+\.js$/i,new e.CacheFirst({cacheName:"next-static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4|webm)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:48,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({sameOrigin:e,url:{pathname:s}})=>!(!e||s.startsWith("/api/auth/callback")||!s.startsWith("/api/")),new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({request:e,url:{pathname:s},sameOrigin:a})=>"1"===e.headers.get("RSC")&&"1"===e.headers.get("Next-Router-Prefetch")&&a&&!s.startsWith("/api/"),new e.NetworkFirst({cacheName:"pages-rsc-prefetch",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({request:e,url:{pathname:s},sameOrigin:a})=>"1"===e.headers.get("RSC")&&a&&!s.startsWith("/api/"),new e.NetworkFirst({cacheName:"pages-rsc",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:{pathname:e},sameOrigin:s})=>s&&!e.startsWith("/api/"),new e.NetworkFirst({cacheName:"pages",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({sameOrigin:e})=>!e,new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET"),self.__WB_DISABLE_DEV_LOGS=!0}); +if(!self.define){let e,s={};const a=(a,n)=>(a=new URL(a+".js",n).href,s[a]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=a,e.onload=s,document.head.appendChild(e)}else e=a,importScripts(a),s()}).then(()=>{let e=s[a];if(!e)throw new Error(`Module ${a} didn’t register its module`);return e}));self.define=(n,i)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let c={};const d=e=>a(e,t),r={module:{uri:t},exports:c,require:d};s[t]=Promise.all(n.map(e=>r[e]||d(e))).then(e=>(i(...e),c))}}define(["./workbox-f1770938"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/chunks/4bd1b696-deba172d32c79f82.js",revision:"deba172d32c79f82"},{url:"/_next/static/chunks/500-b84d19d842172eba.js",revision:"b84d19d842172eba"},{url:"/_next/static/chunks/928-34428627dfa9fa40.js",revision:"34428627dfa9fa40"},{url:"/_next/static/chunks/973-54517c363a97a136.js",revision:"54517c363a97a136"},{url:"/_next/static/chunks/app/_global-error/page-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/app/_not-found/page-f7e9e5d73062cbe5.js",revision:"f7e9e5d73062cbe5"},{url:"/_next/static/chunks/app/layout-83a3f45ebc4f840c.js",revision:"83a3f45ebc4f840c"},{url:"/_next/static/chunks/app/legal/page-d8375d00d5f2fcde.js",revision:"d8375d00d5f2fcde"},{url:"/_next/static/chunks/app/page-054316de7bfce009.js",revision:"054316de7bfce009"},{url:"/_next/static/chunks/app/privacy-policy/page-785b7b2aafe031c2.js",revision:"785b7b2aafe031c2"},{url:"/_next/static/chunks/framework-4e51298db41fcfd4.js",revision:"4e51298db41fcfd4"},{url:"/_next/static/chunks/main-2682d92e5634a569.js",revision:"2682d92e5634a569"},{url:"/_next/static/chunks/main-app-abcb25e568d7cd6e.js",revision:"abcb25e568d7cd6e"},{url:"/_next/static/chunks/next/dist/client/components/builtin/app-error-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/next/dist/client/components/builtin/forbidden-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/next/dist/client/components/builtin/global-error-1dc6a36b040319f2.js",revision:"1dc6a36b040319f2"},{url:"/_next/static/chunks/next/dist/client/components/builtin/not-found-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/next/dist/client/components/builtin/unauthorized-a3d4f8e832675edd.js",revision:"a3d4f8e832675edd"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-07bf70a7cee35489.js",revision:"07bf70a7cee35489"},{url:"/_next/static/css/6fa0bde524d10cf5.css",revision:"6fa0bde524d10cf5"},{url:"/_next/static/kAIdduKPOsHcC7zjdYMUm/_buildManifest.js",revision:"4994a1a64b62b600714e5f6227cd3d29"},{url:"/_next/static/kAIdduKPOsHcC7zjdYMUm/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/media/19cfc7226ec3afaa-s.woff2",revision:"9dda5cfc9a46f256d0e131bb535e46f8"},{url:"/_next/static/media/21350d82a1f187e9-s.woff2",revision:"4e2553027f1d60eff32898367dd4d541"},{url:"/_next/static/media/8e9860b6e62d6359-s.woff2",revision:"01ba6c2a184b8cba08b0d57167664d75"},{url:"/_next/static/media/ba9851c3c22cd980-s.woff2",revision:"9e494903d6b0ffec1a1e14d34427d44d"},{url:"/_next/static/media/c5fe6dc8356a8c31-s.woff2",revision:"027a89e9ab733a145db70f09b8a18b42"},{url:"/_next/static/media/df0a9ae256c0569c-s.woff2",revision:"d54db44de5ccb18886ece2fda72bdfe0"},{url:"/_next/static/media/e4af272ccee01ff0-s.p.woff2",revision:"65850a373e258f1c897a2b3d75eb74de"},{url:"/apple-touch-icon.png",revision:"b73ace5d08ca50ae4d207ae49aa324a7"},{url:"/assets/screenshot-calculator.png",revision:"b5fb22478199d5d0a0567d6c3bd2c05d"},{url:"/assets/screenshot-hangar.png",revision:"ebf24315ea400345ca95724c2f38067f"},{url:"/assets/screenshot-report.png",revision:"153a01d7958719252c31e1a52830eb0f"},{url:"/file.svg",revision:"d09f95206c3fa0bb9bd9fefabfd0ea71"},{url:"/globe.svg",revision:"2aaafa6a49b6563925fe440891e32717"},{url:"/icon-192.png",revision:"4cc4ba11db593a4f3899fc2c628e4bfb"},{url:"/icon-512.png",revision:"808fb568cffbb81a989f7e187df3fcc9"},{url:"/manifest.json",revision:"4b7e0c73a2888962193970e2f5bd7149"},{url:"/next.svg",revision:"8e061864f388b47f33a1c3780831193e"},{url:"/swe-worker-5c72df51bb1f6ee0.js",revision:"76fdd3369f623a3edcf74ce2200bfdd0"},{url:"/vercel.svg",revision:"c0af2f507b369b085b35ef4bbe3bcf1e"},{url:"/window.svg",revision:"a2760511c65806022ad20adf74370ff3"}],{ignoreURLParametersMatching:[/^utm_/,/^fbclid$/]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({response:e})=>e&&"opaqueredirect"===e.type?new Response(e.body,{status:200,statusText:"OK",headers:e.headers}):e}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:2592e3})]}),"GET"),e.registerRoute(/\/_next\/static.+\.js$/i,new e.CacheFirst({cacheName:"next-static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4|webm)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:48,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({sameOrigin:e,url:{pathname:s}})=>!(!e||s.startsWith("/api/auth/callback")||!s.startsWith("/api/")),new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({request:e,url:{pathname:s},sameOrigin:a})=>"1"===e.headers.get("RSC")&&"1"===e.headers.get("Next-Router-Prefetch")&&a&&!s.startsWith("/api/"),new e.NetworkFirst({cacheName:"pages-rsc-prefetch",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({request:e,url:{pathname:s},sameOrigin:a})=>"1"===e.headers.get("RSC")&&a&&!s.startsWith("/api/"),new e.NetworkFirst({cacheName:"pages-rsc",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:{pathname:e},sameOrigin:s})=>s&&!e.startsWith("/api/"),new e.NetworkFirst({cacheName:"pages",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({sameOrigin:e})=>!e,new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET"),self.__WB_DISABLE_DEV_LOGS=!0}); diff --git a/utils/validation.test.ts b/utils/validation.test.ts new file mode 100644 index 0000000..ffff3e8 --- /dev/null +++ b/utils/validation.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect } from 'vitest'; +import { validateImportedFleet } from './validation'; +import { SavedAircraft } from '../data/aircraft'; + +describe('validateImportedFleet', () => { + const validAircraft: SavedAircraft = { + id: 'test-plane', + make: 'Test', + model: 'Plane', + emptyWeight: 1000, + emptyArm: 50, + registration: 'N12345', + isCustomPlane: true, + stations: [ + { id: 'seat1', name: 'Pilot', arm: 40, maxWeight: 200 } + ], + envelope: [ + { cg: 30, weight: 800 }, + { cg: 60, weight: 1200 } + ] + }; + + it('should validate a correct fleet array', () => { + const fleet = [validAircraft]; + expect(validateImportedFleet(fleet)).toEqual(fleet); + }); + + it('should return null for non-array input', () => { + expect(validateImportedFleet({} as any)).toBeNull(); + expect(validateImportedFleet("string" as any)).toBeNull(); + expect(validateImportedFleet(123 as any)).toBeNull(); + }); + + it('should return null if an item is missing required fields', () => { + const invalidPlane = { ...validAircraft, registration: undefined }; + expect(validateImportedFleet([invalidPlane])).toBeNull(); + }); + + it('should return null if stations are invalid', () => { + const invalidPlane = { + ...validAircraft, + stations: [{ id: 'seat1', name: 'Pilot', arm: 'invalid' }] // arm is string + }; + expect(validateImportedFleet([invalidPlane])).toBeNull(); + }); + + it('should return null if envelope is invalid', () => { + const invalidPlane = { + ...validAircraft, + envelope: "not-an-array" + }; + expect(validateImportedFleet([invalidPlane])).toBeNull(); + }); + + it('should validate with optional fields', () => { + const planeWithOptional = { + ...validAircraft, + utilityEnvelope: [{ cg: 30, weight: 800 }], + savedArmOverrides: { seat1: 41 } + }; + expect(validateImportedFleet([planeWithOptional])).toEqual([planeWithOptional]); + }); + + it('should return null for malformed arm overrides', () => { + const invalidPlane = { + ...validAircraft, + savedArmOverrides: { seat1: "should-be-number" } + }; + expect(validateImportedFleet([invalidPlane])).toBeNull(); + }); +}); diff --git a/utils/validation.ts b/utils/validation.ts new file mode 100644 index 0000000..1dd08cc --- /dev/null +++ b/utils/validation.ts @@ -0,0 +1,84 @@ +import { SavedAircraft, Station, EnvelopePoint } from "../data/aircraft"; + +/** + * Validates that the input data matches the SavedAircraft structure. + * Returns the valid SavedAircraft[] or null if invalid. + */ +export function validateImportedFleet(data: any): SavedAircraft[] | null { + if (!Array.isArray(data)) { + return null; + } + + for (const item of data) { + if (!validateSavedAircraft(item)) { + return null; + } + } + + return data as SavedAircraft[]; +} + +function validateSavedAircraft(item: any): boolean { + if (typeof item !== "object" || item === null) return false; + + // Check required primitive fields + if ( + typeof item.id !== "string" || + typeof item.make !== "string" || + typeof item.model !== "string" || + typeof item.emptyWeight !== "number" || + typeof item.emptyArm !== "number" || + typeof item.registration !== "string" || + item.isCustomPlane !== true + ) { + return false; + } + + // Check stations + if (!Array.isArray(item.stations)) return false; + for (const station of item.stations) { + if (!validateStation(station)) return false; + } + + // Check envelope + if (!Array.isArray(item.envelope)) return false; + for (const point of item.envelope) { + if (!validateEnvelopePoint(point)) return false; + } + + // Check utilityEnvelope (optional) + if (item.utilityEnvelope !== undefined) { + if (!Array.isArray(item.utilityEnvelope)) return false; + for (const point of item.utilityEnvelope) { + if (!validateEnvelopePoint(point)) return false; + } + } + + // Check savedArmOverrides (optional) + if (item.savedArmOverrides !== undefined) { + if (typeof item.savedArmOverrides !== "object" || item.savedArmOverrides === null) return false; + for (const key in item.savedArmOverrides) { + if (typeof item.savedArmOverrides[key] !== "number") return false; + } + } + + return true; +} + +function validateStation(station: any): boolean { + if (typeof station !== "object" || station === null) return false; + return ( + typeof station.id === "string" && + typeof station.name === "string" && + typeof station.arm === "number" && + typeof station.maxWeight === "number" + ); +} + +function validateEnvelopePoint(point: any): boolean { + if (typeof point !== "object" || point === null) return false; + return ( + typeof point.cg === "number" && + typeof point.weight === "number" + ); +}