mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
fix: return JSON errors for unmatched local API routes
Test development and built-preview route registration, preserve development-only credential editing, and document local preview configuration.
This commit is contained in:
1 parent
a0f07b752f
commit
280fc00a51
8 files changed
+205
-4
No files matched your search
@@ -13,6 +13,9 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- Restore data-provider routes under local build preview and return JSON 404s
|
||||||
|
for unmatched API requests. Credential editing remains development-only.
|
||||||
|
|
||||||
- Extract CCTV catalog/media and Radio Browser directory providers into focused
|
- Extract CCTV catalog/media and Radio Browser directory providers into focused
|
||||||
Node modules, preserving their routes and policies and isolating CCTV catalogs
|
Node modules, preserving their routes and policies and isolating CCTV catalogs
|
||||||
by provider instance and application root.
|
by provider instance and application root.
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ through Pinokio; the terminal path above remains the contributor path.
|
|||||||
|
|
||||||
Open `http://localhost:4173`. Before sending a PR run `npm run build`, `npm test`, and `npm run test:track` (dev server must be up) — **all three must stay green.**
|
Open `http://localhost:4173`. Before sending a PR run `npm run build`, `npm test`, and `npm run test:track` (dev server must be up) — **all three must stay green.**
|
||||||
|
|
||||||
|
## Checking a built app locally
|
||||||
|
|
||||||
|
Run `npm run build` followed by `npm run preview`. Preview serves the built
|
||||||
|
frontend and local data-provider APIs. Keep optional server credentials in the
|
||||||
|
ignored `.env`; browser keys are embedded during the build, so rebuild after
|
||||||
|
changing them. Provider Settings and `/api/setup/*` are development-only: edit
|
||||||
|
configuration through the development app or environment file. Unknown API
|
||||||
|
paths return JSON 404 responses. Vite preview is for checking a local build;
|
||||||
|
it is not a production server.
|
||||||
|
|
||||||
## Good first contributions
|
## Good first contributions
|
||||||
|
|
||||||
The highest-leverage places to jump in:
|
The highest-leverage places to jump in:
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
# God's Eye View Current State
|
# God's Eye View Current State
|
||||||
|
|
||||||
|
## Local build preview
|
||||||
|
|
||||||
|
After `npm run build`, `npm run preview` serves the built app with the same data
|
||||||
|
provider routes as development, including aircraft, satellites, terrain, traffic,
|
||||||
|
FIRMS, GBFS, Overpass and CCTV/media. Unmatched `/api` requests return a JSON 404
|
||||||
|
in both modes instead of the application HTML. Browser routes retain SPA fallback.
|
||||||
|
Credential editing (`/api/setup/*` and Provider Settings) is development-only;
|
||||||
|
preview returns JSON 404 for those endpoints. Server credentials come from the
|
||||||
|
local environment; browser keys are captured at build time. Rebuild after changing
|
||||||
|
a browser key. Preview is for local build verification, not a production server.
|
||||||
|
|
||||||
|
|
||||||
## CCTV and radio provider modules
|
## CCTV and radio provider modules
|
||||||
|
|
||||||
CCTV catalog acquisition, source normalization and frame/media delivery now live
|
CCTV catalog acquisition, source normalization and frame/media delivery now live
|
||||||
|
|||||||
@@ -81,5 +81,7 @@
|
|||||||
"server/providers/radio/constants.js",
|
"server/providers/radio/constants.js",
|
||||||
"server/providers/radio/stations.js",
|
"server/providers/radio/stations.js",
|
||||||
"server/providers/radio/transport.js",
|
"server/providers/radio/transport.js",
|
||||||
"src/tooling/mediaProviders.test.mjs"
|
"src/tooling/mediaProviders.test.mjs",
|
||||||
|
"server/standalone/api-not-found.js",
|
||||||
|
"src/tooling/previewServing.test.mjs"
|
||||||
]
|
]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/** Finish unmatched API requests before Vite's HTML fallback. Install last. */
|
||||||
|
export function apiNotFoundPlugin() {
|
||||||
|
const install = (server) => {
|
||||||
|
server.middlewares.use('/api', (_req, res) => {
|
||||||
|
res.writeHead(404, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
});
|
||||||
|
res.end(JSON.stringify({ error: 'Unknown API route' }));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
name: 'api-not-found',
|
||||||
|
configureServer: install,
|
||||||
|
configurePreviewServer: install,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { fileURLToPath } from 'node:url';
|
|||||||
import { defineConfig, loadEnv } from 'vite';
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
import { createBrowserViteConfig } from '../../build/vite.js';
|
import { createBrowserViteConfig } from '../../build/vite.js';
|
||||||
import { localProviderPlugins } from '../providers/local.js';
|
import { localProviderPlugins } from '../providers/local.js';
|
||||||
|
import { apiNotFoundPlugin } from './api-not-found.js';
|
||||||
|
|
||||||
const root = fileURLToPath(new URL('../../', import.meta.url));
|
const root = fileURLToPath(new URL('../../', import.meta.url));
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
if (process.env[key] === undefined) process.env[key] = value;
|
if (process.env[key] === undefined) process.env[key] = value;
|
||||||
}
|
}
|
||||||
return createBrowserViteConfig({
|
return createBrowserViteConfig({
|
||||||
plugins: localProviderPlugins(),
|
plugins: [...localProviderPlugins(), apiNotFoundPlugin()],
|
||||||
googleApiKey: process.env.GOOGLE_MAPS_API_KEY,
|
googleApiKey: process.env.GOOGLE_MAPS_API_KEY,
|
||||||
cesiumToken: process.env.CESIUM_ION_TOKEN,
|
cesiumToken: process.env.CESIUM_ION_TOKEN,
|
||||||
host: process.env.HOST,
|
host: process.env.HOST,
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { build, createServer, preview } from 'vite';
|
||||||
|
import { localProviderPlugins } from '../../server/providers/local.js';
|
||||||
|
import { apiNotFoundPlugin } from '../../server/standalone/api-not-found.js';
|
||||||
|
|
||||||
|
test('data providers have both hooks; credential editing stays development-only', () => {
|
||||||
|
for (const plugin of localProviderPlugins()) {
|
||||||
|
if (plugin.name === 'gev-key-setup') {
|
||||||
|
assert.equal(plugin.configurePreviewServer, undefined);
|
||||||
|
assert.equal(
|
||||||
|
plugin.apply({}, { command: 'serve', isPreview: true }),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
plugin.apply({}, { command: 'serve', isPreview: false }),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assert.equal(typeof plugin.configureServer, 'function', plugin.name);
|
||||||
|
assert.equal(typeof plugin.configurePreviewServer, 'function', plugin.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('real dev and built-preview servers serve provider JSON and terminate unknown APIs', async (t) => {
|
||||||
|
const root = await mkdtemp(path.join(tmpdir(), 'gev-preview-'));
|
||||||
|
t.after(() => rm(root, { recursive: true, force: true }));
|
||||||
|
await writeFile(
|
||||||
|
path.join(root, 'index.html'),
|
||||||
|
'<!doctype html><title>Preview fixture</title><p>Built application</p>',
|
||||||
|
);
|
||||||
|
for (const [name, value] of Object.entries({
|
||||||
|
AISSTREAM_API_KEY: '',
|
||||||
|
FIRMS_MAP_KEY: '',
|
||||||
|
FIRMS_API_KEY: '',
|
||||||
|
TOMTOM_API_KEY: '',
|
||||||
|
GOOGLE_MAPS_API_KEY: '',
|
||||||
|
GOOGLE_MAPS_SERVER_API_KEY: '',
|
||||||
|
OPENAI_API_KEY: '',
|
||||||
|
OPENSKY_AUTH_MODE: 'anon',
|
||||||
|
OPENSKY_CLIENT_ID: '',
|
||||||
|
OPENSKY_CLIENT_SECRET: '',
|
||||||
|
CCTV_FORCE_AUSTIN: '0',
|
||||||
|
CCTV_SOURCES_FILE: path.join(root, 'absent.json'),
|
||||||
|
CCTV_SOURCES_JSON: JSON.stringify([
|
||||||
|
{ id: 'fixture', lat: 30.27, lon: -97.74 },
|
||||||
|
]),
|
||||||
|
})) {
|
||||||
|
const before = process.env[name];
|
||||||
|
process.env[name] = value;
|
||||||
|
t.after(() => {
|
||||||
|
if (before === undefined) delete process.env[name];
|
||||||
|
else process.env[name] = before;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const nativeFetch = globalThis.fetch;
|
||||||
|
t.mock.method(globalThis, 'fetch', async (raw, options) => {
|
||||||
|
const url = new URL(raw);
|
||||||
|
if (url.hostname === '127.0.0.1') return nativeFetch(raw, options);
|
||||||
|
assert.ok(
|
||||||
|
['opensky-network.org', 'api.adsb.lol'].includes(url.hostname),
|
||||||
|
url.origin,
|
||||||
|
);
|
||||||
|
return Response.json({
|
||||||
|
time: Math.floor(Date.now() / 1000),
|
||||||
|
states: [],
|
||||||
|
ac: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const base = {
|
||||||
|
root,
|
||||||
|
configFile: false,
|
||||||
|
envFile: false,
|
||||||
|
publicDir: false,
|
||||||
|
logLevel: 'silent',
|
||||||
|
};
|
||||||
|
await build(base);
|
||||||
|
for (const isPreview of [false, true]) {
|
||||||
|
const config = {
|
||||||
|
...base,
|
||||||
|
plugins: [...localProviderPlugins(), apiNotFoundPlugin()],
|
||||||
|
server: { host: '127.0.0.1', port: 0, hmr: false },
|
||||||
|
preview: { host: '127.0.0.1', port: 0 },
|
||||||
|
};
|
||||||
|
const server = isPreview
|
||||||
|
? await preview(config)
|
||||||
|
: await createServer(config);
|
||||||
|
if (!isPreview) await server.listen();
|
||||||
|
const origin = `http://127.0.0.1:${server.httpServer.address().port}`;
|
||||||
|
try {
|
||||||
|
for (const [route, status] of [
|
||||||
|
['/api/opensky', 200],
|
||||||
|
['/api/adsblol/mil', 200],
|
||||||
|
['/api/adsbdb/type/invalid', 400],
|
||||||
|
['/api/firms/status', 200],
|
||||||
|
['/api/terrain/heights?points=invalid', 400],
|
||||||
|
['/api/overpass', 405],
|
||||||
|
['/api/cctv/sources', 200],
|
||||||
|
['/api/gbfs/', 400],
|
||||||
|
['/api/tomtom/status', 200],
|
||||||
|
['/api/radio/unknown', 404],
|
||||||
|
['/api/setup/status', isPreview ? 404 : 200],
|
||||||
|
['/api/setup/update', 404],
|
||||||
|
['/api/does-not-exist', 404],
|
||||||
|
['/api', 404],
|
||||||
|
]) {
|
||||||
|
const response = await fetch(origin + route);
|
||||||
|
assert.equal(
|
||||||
|
response.status,
|
||||||
|
status,
|
||||||
|
`${isPreview ? 'preview' : 'dev'} ${route}`,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
response.headers.get('content-type'),
|
||||||
|
/application\/json/,
|
||||||
|
route,
|
||||||
|
);
|
||||||
|
const body = await response.json();
|
||||||
|
if (route === '/api/cctv/sources')
|
||||||
|
assert.equal(body.sources[0].id, 'fixture');
|
||||||
|
if (
|
||||||
|
route === '/api/does-not-exist' ||
|
||||||
|
(isPreview && route.startsWith('/api/setup'))
|
||||||
|
) {
|
||||||
|
assert.deepEqual(body, { error: 'Unknown API route' });
|
||||||
|
assert.equal(response.headers.get('cache-control'), 'no-store');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const tle = await fetch(origin + '/api/celestrak/invalid!');
|
||||||
|
assert.equal(tle.status, 400);
|
||||||
|
assert.equal(await tle.text(), 'invalid group');
|
||||||
|
if (isPreview) {
|
||||||
|
const write = await fetch(origin + '/api/setup/keys', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ GOOGLE_MAPS_API_KEY: 'must-not-save' }),
|
||||||
|
});
|
||||||
|
assert.equal(write.status, 404);
|
||||||
|
assert.deepEqual(await write.json(), { error: 'Unknown API route' });
|
||||||
|
}
|
||||||
|
for (const route of ['/', '/application-route', '/apiary']) {
|
||||||
|
const response = await fetch(origin + route);
|
||||||
|
assert.equal(response.status, 200);
|
||||||
|
assert.match(response.headers.get('content-type'), /text\/html/);
|
||||||
|
assert.match(await response.text(), /Built application/);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await server.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -63,10 +63,11 @@ test('root config retains existing named exports and standalone provider order',
|
|||||||
assert.equal(compatibility[name], value, name);
|
assert.equal(compatibility[name], value, name);
|
||||||
const config = standaloneConfig({ mode: 'test' });
|
const config = standaloneConfig({ mode: 'test' });
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
config.plugins.slice(1).map((plugin) => plugin.name),
|
config.plugins.slice(1, -1).map((plugin) => plugin.name),
|
||||||
providers.localProviderPlugins().map((plugin) => plugin.name),
|
providers.localProviderPlugins().map((plugin) => plugin.name),
|
||||||
);
|
);
|
||||||
assert.equal(config.plugins.at(-1).name, 'gev-key-setup');
|
assert.equal(config.plugins.at(-2).name, 'gev-key-setup');
|
||||||
|
assert.equal(config.plugins.at(-1).name, 'api-not-found');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('build export resolves in Node and has no browser fallback', async () => {
|
test('build export resolves in Node and has no browser fallback', async () => {
|
||||||
|
|||||||
Reference in new issue
Block a user