smoke: REST API regression harness + baseline snapshots (Meteor 1.6.1.1)
Adds a standalone Node smoke test (smoke/) that exercises every REST endpoint consumed by the Flutter app against a seeded local dev server and compares responses to committed snapshots. This is the safety net for the Meteor upgrade: it must stay byte-identical after every escala. - smoke/seed.js: deterministic Mongo seed (fixed ObjectIds, geo indexes) - smoke/run.js: calls endpoints, normalizes volatile fields, diffs snapshots - smoke/smoke.sh: seed + run wrapper - smoke/snapshots/: baseline captured on Meteor 1.6.1.1 - IPGeocoder.js: degrade gracefully when MaxMind DB absent (dev boot) - settings-development.json: dev-only internalApiToken to enable the REST API - .meteorignore: exclude smoke/ from the Meteor server build
This commit is contained in:
parent
39415808bf
commit
296dab4f38
18 changed files with 539 additions and 4 deletions
|
|
@ -1,3 +1,5 @@
|
||||||
# Don't reload meteor server when we modify or do test
|
# Don't reload meteor server when we modify or do test
|
||||||
cucumber
|
cucumber
|
||||||
output.json
|
output.json
|
||||||
|
# REST smoke-test harness runs standalone (Node), never as Meteor server code
|
||||||
|
smoke
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,16 @@ function isPrivateIP(ip) {
|
||||||
const dbpath = '/usr/local/share/maxmind-geolite2/GeoLite2-City.mmdb';
|
const dbpath = '/usr/local/share/maxmind-geolite2/GeoLite2-City.mmdb';
|
||||||
// const dbpath = `${process.env.PWD}/private/GeoLite2-City.mmdb`;
|
// const dbpath = `${process.env.PWD}/private/GeoLite2-City.mmdb`;
|
||||||
|
|
||||||
if (!fs.existsSync(dbpath)) {
|
let IPGeocoder;
|
||||||
|
if (fs.existsSync(dbpath)) {
|
||||||
|
IPGeocoder = maxmind.openSync(dbpath);
|
||||||
|
} else {
|
||||||
|
// In production the DB is provisioned via cron; in local dev it may be
|
||||||
|
// absent. Degrade gracefully instead of crashing at boot: localize() below
|
||||||
|
// already falls back to a default location when no geo data is available.
|
||||||
console.error(`Maxmind db not found ${dbpath}, download via cron with https://www.npmjs.com/package/maxmind-geolite2-mirror`);
|
console.error(`Maxmind db not found ${dbpath}, download via cron with https://www.npmjs.com/package/maxmind-geolite2-mirror`);
|
||||||
|
IPGeocoder = { get() { return null; } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const IPGeocoder = maxmind.openSync(dbpath);
|
|
||||||
export default IPGeocoder;
|
export default IPGeocoder;
|
||||||
|
|
||||||
// Warning: Meteor cannot access to this.connection with arrow functions
|
// Warning: Meteor cannot access to this.connection with arrow functions
|
||||||
|
|
@ -47,7 +52,7 @@ export function localize() {
|
||||||
// http://dev.maxmind.com/geoip/geoip2/geolite2/
|
// http://dev.maxmind.com/geoip/geoip2/geolite2/
|
||||||
const geo = IPGeocoder.get(clientIP);
|
const geo = IPGeocoder.get(clientIP);
|
||||||
// console.warn(geo);
|
// console.warn(geo);
|
||||||
if (geo.location && geo.location.latitude && geo.location.longitude) {
|
if (geo && geo.location && geo.location.latitude && geo.location.longitude) {
|
||||||
return geo;
|
return geo;
|
||||||
}
|
}
|
||||||
// geoIP fallback, Madrid
|
// geoIP fallback, Madrid
|
||||||
|
|
|
||||||
73
smoke/README.md
Normal file
73
smoke/README.md
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
# REST smoke test
|
||||||
|
|
||||||
|
Regression harness for the REST API in `imports/api/Rest/Rest.js` — the API the
|
||||||
|
Flutter app (`fires_flutter/lib/models/fires_api.dart`) depends on.
|
||||||
|
|
||||||
|
It calls every Flutter-consumed endpoint against a locally running dev server
|
||||||
|
with deterministic seed data, normalizes the intrinsically volatile fields
|
||||||
|
(uptime, generated ids, insert timestamps) and compares the responses to
|
||||||
|
committed snapshots in `smoke/snapshots/`.
|
||||||
|
|
||||||
|
**It must stay green after every Meteor upgrade escala. The snapshots are the
|
||||||
|
contract: any drift is treated as an upgrade bug.**
|
||||||
|
|
||||||
|
## Reproducible local startup
|
||||||
|
|
||||||
|
1. **MongoDB** (prod parity = 3.2; a dev-only instance in Docker):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name tcef-mongo32 -p 27018:27017 mongo:3.2
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Meteor dev server** (from the repo root):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PATH="$HOME/.meteor:$PATH"
|
||||||
|
export MONGO_URL="mongodb://localhost:27018/fuegos"
|
||||||
|
meteor --settings settings-development.json --port 3100
|
||||||
|
```
|
||||||
|
|
||||||
|
`settings-development.json` carries a dev-only `private.internalApiToken`
|
||||||
|
(`dev-smoke-token`) which is what enables the REST API. Without it the API
|
||||||
|
routes are not even registered (see `Rest.js`).
|
||||||
|
|
||||||
|
> Note: `IPGeocoder.js` degrades gracefully when the MaxMind GeoLite2 DB is
|
||||||
|
> absent (it is provisioned by cron in production) so the server boots in a
|
||||||
|
> clean checkout.
|
||||||
|
|
||||||
|
## Running the smoke test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./smoke/smoke.sh # seed + compare against baseline snapshots
|
||||||
|
./smoke/smoke.sh --update # seed + regenerate the baseline snapshots
|
||||||
|
```
|
||||||
|
|
||||||
|
Env overrides: `MONGO_CONTAINER` (default `tcef-mongo32`), `MONGO_DB`
|
||||||
|
(`fuegos`), `BASE_URL` (`http://localhost:3100`).
|
||||||
|
|
||||||
|
## What is covered
|
||||||
|
|
||||||
|
| Snapshot | Endpoint | Flutter call |
|
||||||
|
|---|---|---|
|
||||||
|
| `status_uptime` | `GET status/uptime` | — |
|
||||||
|
| `status_active_fires_count` | `GET status/active-fires-count` | — |
|
||||||
|
| `status_last_fire_check` | `GET status/last-fire-check` | — |
|
||||||
|
| `status_last_fire_detected` | `GET status/last-fire-detected` | — |
|
||||||
|
| `status_subs_public_union` | `GET status/subs-public-union/:token` | `getMonitoredAreas` |
|
||||||
|
| `fires_in` | `GET fires-in/:token/:lat/:lng/:km` | — |
|
||||||
|
| `fires_in_full` | `GET fires-in-full/:token/:lat/:lng/:km` | `getFiresInLocation` |
|
||||||
|
| `mobile_users_post` | `POST mobile/users` | `createUser` |
|
||||||
|
| `mobile_subscriptions_post` | `POST mobile/subscriptions` | `subscribe` |
|
||||||
|
| `mobile_subscriptions_all` | `GET mobile/subscriptions/all/:token/:mobileToken` | `fetchYourLocations` |
|
||||||
|
| `mobile_subscriptions_delete` | `DELETE mobile/subscriptions/:token/:mobileToken/:subsId` | `unsubscribe` |
|
||||||
|
| `mobile_falsepositive_post` | `POST mobile/falsepositive` | `markFalsePositive` |
|
||||||
|
|
||||||
|
## Normalized (volatile) fields
|
||||||
|
|
||||||
|
Compared structurally but blanked to a placeholder because they cannot be
|
||||||
|
deterministic: `status/uptime` → `ms`; `mobile/users` → `userId`, `username`,
|
||||||
|
`upsertResult.insertedId`; `mobile/subscriptions/all` → `owner`, `createdAt`,
|
||||||
|
`updatedAt`; `mobile/falsepositive` → `upsert.insertedId`.
|
||||||
|
|
||||||
|
Everything else (ids of seeded docs, geometries, counts, jsend envelope, field
|
||||||
|
names and types) is asserted byte-for-byte.
|
||||||
172
smoke/run.js
Executable file
172
smoke/run.js
Executable file
|
|
@ -0,0 +1,172 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/*
|
||||||
|
* REST smoke test for the Flutter-consumed API of todos-contra-el-fuego-web.
|
||||||
|
*
|
||||||
|
* Calls every endpoint the Flutter app (fires_flutter/lib/models/fires_api.dart)
|
||||||
|
* depends on, normalizes the intrinsically volatile fields (uptime, generated
|
||||||
|
* ids, insert timestamps), and compares the result against committed snapshots.
|
||||||
|
*
|
||||||
|
* The responses MUST stay identical across every Meteor upgrade escala — the
|
||||||
|
* app parses them field by field.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node smoke/run.js --update # (re)write baseline snapshots
|
||||||
|
* node smoke/run.js # compare against baseline, exit 1 on drift
|
||||||
|
*
|
||||||
|
* Env:
|
||||||
|
* BASE_URL default http://localhost:3100
|
||||||
|
* SETTINGS default settings-development.json (token + ironPassword)
|
||||||
|
*
|
||||||
|
* Requires the dev server running and the DB seeded (smoke/seed.sh does both).
|
||||||
|
*/
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname, '..');
|
||||||
|
const SNAP_DIR = path.join(__dirname, 'snapshots');
|
||||||
|
const BASE = (process.env.BASE_URL || 'http://localhost:3100').replace(/\/$/, '');
|
||||||
|
const API = `${BASE}/api/v1`;
|
||||||
|
const UPDATE = process.argv.includes('--update');
|
||||||
|
|
||||||
|
const settings = JSON.parse(fs.readFileSync(path.join(ROOT, process.env.SETTINGS || 'settings-development.json'), 'utf8'));
|
||||||
|
const TOKEN = settings.private.internalApiToken;
|
||||||
|
const IRON_PASSWORD = settings.private.ironPassword;
|
||||||
|
|
||||||
|
// Fixed inputs — must match smoke/seed.js
|
||||||
|
const MOBILE_TOKEN = 'smoke-mobile-token';
|
||||||
|
const SUBS_ID = 'd0000000000000000000000d';
|
||||||
|
const LAT = 40.41;
|
||||||
|
const LON = -3.70;
|
||||||
|
|
||||||
|
// ---------- helpers ----------
|
||||||
|
function stableStringify(v) {
|
||||||
|
return JSON.stringify(sortKeys(v), null, 2);
|
||||||
|
}
|
||||||
|
function sortKeys(v) {
|
||||||
|
if (Array.isArray(v)) return v.map(sortKeys);
|
||||||
|
if (v && typeof v === 'object') {
|
||||||
|
return Object.keys(v).sort().reduce((acc, k) => { acc[k] = sortKeys(v[k]); return acc; }, {});
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
async function req(method, url, body) {
|
||||||
|
const opts = { method, headers: {} };
|
||||||
|
if (body !== undefined) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); }
|
||||||
|
const res = await fetch(url, opts);
|
||||||
|
const text = await res.text();
|
||||||
|
let json;
|
||||||
|
try { json = JSON.parse(text); } catch (e) { json = { __nonJson: text }; }
|
||||||
|
return { status: res.status, json };
|
||||||
|
}
|
||||||
|
// Replace volatile fields (by key name) with a stable placeholder, recursively.
|
||||||
|
function scrub(obj, keys) {
|
||||||
|
if (Array.isArray(obj)) return obj.map(o => scrub(o, keys));
|
||||||
|
if (obj && typeof obj === 'object') {
|
||||||
|
const out = {};
|
||||||
|
for (const k of Object.keys(obj)) {
|
||||||
|
out[k] = keys.includes(k) ? `<${k}>` : scrub(obj[k], keys);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- iron seal for mobile/falsepositive ----------
|
||||||
|
async function sealFire() {
|
||||||
|
const Iron = require('iron');
|
||||||
|
const fire = {
|
||||||
|
ourid: { type: 'Point', coordinates: [LON, LAT] },
|
||||||
|
lat: LAT, lon: LON, type: 'viirs',
|
||||||
|
when: '2024-01-01T00:00:00.000Z',
|
||||||
|
scan: 0.5, track: 0.4, address: 'Madrid, Spain'
|
||||||
|
};
|
||||||
|
// iron v5 seal returns a promise
|
||||||
|
return Iron.seal(fire, IRON_PASSWORD, Iron.defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- test sequence ----------
|
||||||
|
async function collect() {
|
||||||
|
const snaps = {};
|
||||||
|
|
||||||
|
snaps['status_uptime'] = scrub((await req('GET', `${API}/status/uptime`)).json, ['ms']);
|
||||||
|
snaps['status_active_fires_count'] = (await req('GET', `${API}/status/active-fires-count`)).json;
|
||||||
|
snaps['status_last_fire_check'] = (await req('GET', `${API}/status/last-fire-check`)).json;
|
||||||
|
snaps['status_last_fire_detected'] = (await req('GET', `${API}/status/last-fire-detected`)).json;
|
||||||
|
snaps['status_subs_public_union'] = (await req('GET', `${API}/status/subs-public-union/${TOKEN}`)).json;
|
||||||
|
|
||||||
|
// Read-only fires query BEFORE any false-positive is inserted.
|
||||||
|
snaps['fires_in_full'] = (await req('GET', `${API}/fires-in-full/${TOKEN}/${LAT}/${LON}/100`)).json;
|
||||||
|
snaps['fires_in'] = (await req('GET', `${API}/fires-in/${TOKEN}/${LAT}/${LON}/100`)).json;
|
||||||
|
|
||||||
|
// Create mobile user
|
||||||
|
const users = await req('POST', `${API}/mobile/users`, { token: TOKEN, mobileToken: MOBILE_TOKEN, lang: 'es' });
|
||||||
|
snaps['mobile_users_post'] = scrub(users.json, ['userId', 'username', 'insertedId']);
|
||||||
|
|
||||||
|
// Subscribe
|
||||||
|
const sub = await req('POST', `${API}/mobile/subscriptions`, {
|
||||||
|
token: TOKEN, mobileToken: MOBILE_TOKEN, id: SUBS_ID, lat: LAT, lon: LON, distance: 40
|
||||||
|
});
|
||||||
|
snaps['mobile_subscriptions_post'] = sub.json;
|
||||||
|
|
||||||
|
// List subscriptions
|
||||||
|
const list = await req('GET', `${API}/mobile/subscriptions/all/${TOKEN}/${MOBILE_TOKEN}`);
|
||||||
|
snaps['mobile_subscriptions_all'] = scrub(list.json, ['owner', 'createdAt', 'updatedAt']);
|
||||||
|
|
||||||
|
// Delete the subscription
|
||||||
|
const del = await req('DELETE', `${API}/mobile/subscriptions/${TOKEN}/${MOBILE_TOKEN}/${SUBS_ID}`);
|
||||||
|
snaps['mobile_subscriptions_delete'] = del.json;
|
||||||
|
|
||||||
|
// False positive (LAST — mutates falsePositives collection)
|
||||||
|
const sealed = await sealFire();
|
||||||
|
const fp = await req('POST', `${API}/mobile/falsepositive`, {
|
||||||
|
token: TOKEN, mobileToken: MOBILE_TOKEN, sealed, type: 'falsealarm'
|
||||||
|
});
|
||||||
|
snaps['mobile_falsepositive_post'] = scrub(fp.json, ['insertedId']);
|
||||||
|
|
||||||
|
return snaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (!TOKEN) { console.error('No internalApiToken in settings'); process.exit(2); }
|
||||||
|
fs.mkdirSync(SNAP_DIR, { recursive: true });
|
||||||
|
const snaps = await collect();
|
||||||
|
|
||||||
|
let fail = 0;
|
||||||
|
for (const [name, value] of Object.entries(snaps)) {
|
||||||
|
const file = path.join(SNAP_DIR, `${name}.json`);
|
||||||
|
const got = stableStringify(value);
|
||||||
|
if (UPDATE) {
|
||||||
|
fs.writeFileSync(file, `${got}\n`);
|
||||||
|
console.log(` wrote ${name}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(file)) { console.error(` MISSING baseline: ${name}`); fail++; continue; }
|
||||||
|
const want = fs.readFileSync(file, 'utf8').trimEnd();
|
||||||
|
if (want === got.trimEnd()) {
|
||||||
|
console.log(` PASS ${name}`);
|
||||||
|
} else {
|
||||||
|
fail++;
|
||||||
|
console.error(` FAIL ${name}`);
|
||||||
|
console.error(diff(want, got));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (UPDATE) { console.log('baseline updated'); return; }
|
||||||
|
if (fail) { console.error(`\n${fail} snapshot(s) drifted`); process.exit(1); }
|
||||||
|
console.log('\nAll REST snapshots identical ✔');
|
||||||
|
}
|
||||||
|
|
||||||
|
function diff(a, b) {
|
||||||
|
const al = a.split('\n');
|
||||||
|
const bl = b.split('\n');
|
||||||
|
const out = [];
|
||||||
|
const n = Math.max(al.length, bl.length);
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
if (al[i] !== bl[i]) {
|
||||||
|
if (al[i] !== undefined) out.push(` - ${al[i]}`);
|
||||||
|
if (bl[i] !== undefined) out.push(` + ${bl[i]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.slice(0, 40).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => { console.error(e); process.exit(2); });
|
||||||
96
smoke/seed.js
Normal file
96
smoke/seed.js
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* Deterministic seed for the REST smoke test.
|
||||||
|
* Run against the local Mongo used by the dev Meteor server, e.g.:
|
||||||
|
* docker exec -i tcef-mongo32 mongo fuegos < smoke/seed.js
|
||||||
|
*
|
||||||
|
* It resets the collections the REST API reads/writes to a fixed, known state
|
||||||
|
* so that endpoint responses are reproducible across Meteor upgrade escalas.
|
||||||
|
* Only test-owned documents are touched.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// --- Fixed identifiers (24-hex ObjectIds) -------------------------------
|
||||||
|
var FIRE_A = ObjectId('a0000000000000000000000a');
|
||||||
|
var FIRE_B = ObjectId('b0000000000000000000000b');
|
||||||
|
var ARCHIVE_FIRE = ObjectId('c0000000000000000000000c');
|
||||||
|
var MOBILE_TOKEN = 'smoke-mobile-token';
|
||||||
|
|
||||||
|
// Madrid-ish coordinates
|
||||||
|
var LAT = 40.41;
|
||||||
|
var LON = -3.70;
|
||||||
|
|
||||||
|
var WHEN = ISODate('2024-01-01T00:00:00.000Z');
|
||||||
|
var WHEN_B = ISODate('2024-01-02T00:00:00.000Z'); // later, so last-fire-detected is deterministic
|
||||||
|
var FIXED = ISODate('2024-01-01T00:00:00.000Z');
|
||||||
|
|
||||||
|
// --- activefires --------------------------------------------------------
|
||||||
|
db.activefires.deleteMany({});
|
||||||
|
db.activefires.insertMany([
|
||||||
|
{
|
||||||
|
_id: FIRE_A,
|
||||||
|
ourid: { type: 'Point', coordinates: [LON, LAT] },
|
||||||
|
lat: LAT, lon: LON, type: 'viirs', when: WHEN,
|
||||||
|
scan: 0.5, track: 0.4, acq_date: '2024-01-01', acq_time: '00:00',
|
||||||
|
satellite: 'N', confidence: 50, version: '1.0NRT', frp: 4.3,
|
||||||
|
daynight: 'N', bright_ti4: 330.2, bright_ti5: 291,
|
||||||
|
createdAt: FIXED, updatedAt: FIXED
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: FIRE_B,
|
||||||
|
ourid: { type: 'Point', coordinates: [LON + 0.02, LAT + 0.02] },
|
||||||
|
lat: LAT + 0.02, lon: LON + 0.02, type: 'viirs', when: WHEN_B,
|
||||||
|
scan: 0.5, track: 0.4, acq_date: '2024-01-01', acq_time: '00:05',
|
||||||
|
satellite: 'N', confidence: 60, version: '1.0NRT', frp: 6.1,
|
||||||
|
daynight: 'N', bright_ti4: 331.0, bright_ti5: 292,
|
||||||
|
createdAt: FIXED, updatedAt: FIXED
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
db.activefires.createIndex({ ourid: '2dsphere' });
|
||||||
|
|
||||||
|
// --- fires (archive) — must match the sealed fire used by mobile/falsepositive
|
||||||
|
db.fires.deleteMany({});
|
||||||
|
db.fires.insertOne({
|
||||||
|
_id: ARCHIVE_FIRE,
|
||||||
|
ourid: { type: 'Point', coordinates: [LON, LAT] },
|
||||||
|
lat: LAT, lon: LON, type: 'viirs', when: WHEN,
|
||||||
|
scan: 0.5, track: 0.4, address: 'Madrid, Spain',
|
||||||
|
createdAt: FIXED, updatedAt: FIXED
|
||||||
|
});
|
||||||
|
db.fires.createIndex({ ourid: '2dsphere' });
|
||||||
|
|
||||||
|
// --- falsePositives / industries: empty so "real fire" counting is stable
|
||||||
|
db.falsePositives.deleteMany({});
|
||||||
|
db.industries.deleteMany({});
|
||||||
|
db.industries.createIndex({ geo: '2dsphere' });
|
||||||
|
|
||||||
|
// --- subscriptions: clean; index for geo queries
|
||||||
|
db.subscriptions.deleteMany({});
|
||||||
|
db.subscriptions.createIndex({ geo: '2dsphere' });
|
||||||
|
|
||||||
|
// --- siteSettings consumed by status endpoints --------------------------
|
||||||
|
db.siteSettings.deleteMany({ name: { $in: ['last-fire-check', 'subs-public-union', 'subs-public-union-bounds'] } });
|
||||||
|
db.siteSettings.insertMany([
|
||||||
|
{ _id: ObjectId('51e00000000000000000000c'), name: 'last-fire-check', value: FIXED, createdAt: FIXED, updatedAt: FIXED },
|
||||||
|
{
|
||||||
|
_id: ObjectId('51e0000000000000000000a1'),
|
||||||
|
name: 'subs-public-union',
|
||||||
|
value: JSON.stringify({
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'MultiPolygon',
|
||||||
|
coordinates: [[[[LON - 1, LAT - 1], [LON + 1, LAT - 1], [LON + 1, LAT + 1], [LON - 1, LAT + 1], [LON - 1, LAT - 1]]]]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
createdAt: FIXED, updatedAt: FIXED
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: ObjectId('51e0000000000000000000b2'),
|
||||||
|
name: 'subs-public-union-bounds',
|
||||||
|
value: JSON.stringify({ ne: { lat: LAT + 1, lon: LON + 1 }, sw: { lat: LAT - 1, lon: LON - 1 } }),
|
||||||
|
createdAt: FIXED, updatedAt: FIXED
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// --- users: remove any previous smoke user so mobile/users creates fresh
|
||||||
|
db.users.deleteMany({ fireBaseToken: MOBILE_TOKEN });
|
||||||
|
|
||||||
|
print('seed OK: activefires=' + db.activefires.count() + ' fires=' + db.fires.count() + ' siteSettings=' + db.siteSettings.count({ name: /fire|union/ }));
|
||||||
25
smoke/smoke.sh
Executable file
25
smoke/smoke.sh
Executable file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Seed the dev Mongo deterministically and run the REST smoke test.
|
||||||
|
#
|
||||||
|
# smoke/smoke.sh # compare against committed baseline snapshots
|
||||||
|
# smoke/smoke.sh --update # (re)generate the baseline snapshots
|
||||||
|
#
|
||||||
|
# Env overrides:
|
||||||
|
# MONGO_CONTAINER docker container running the dev Mongo (default tcef-mongo32)
|
||||||
|
# MONGO_DB database name (default fuegos)
|
||||||
|
# BASE_URL dev server URL (default http://localhost:3100)
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
MONGO_CONTAINER="${MONGO_CONTAINER:-tcef-mongo32}"
|
||||||
|
MONGO_DB="${MONGO_DB:-fuegos}"
|
||||||
|
|
||||||
|
echo "== Seeding ${MONGO_DB} in ${MONGO_CONTAINER} =="
|
||||||
|
# Copy the script in and run it as a whole file: the mongo shell evaluates a
|
||||||
|
# file argument atomically (piping via stdin is line-by-line and breaks on
|
||||||
|
# multi-line comments / var scope).
|
||||||
|
docker cp smoke/seed.js "${MONGO_CONTAINER}:/tmp/tcef-seed.js"
|
||||||
|
docker exec "${MONGO_CONTAINER}" mongo "${MONGO_DB}" /tmp/tcef-seed.js | tail -1
|
||||||
|
|
||||||
|
echo "== Running REST smoke test against ${BASE_URL:-http://localhost:3100} =="
|
||||||
|
node smoke/run.js "$@"
|
||||||
4
smoke/snapshots/fires_in.json
Normal file
4
smoke/snapshots/fires_in.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"real": 2,
|
||||||
|
"total": 2
|
||||||
|
}
|
||||||
30
smoke/snapshots/fires_in_full.json
Normal file
30
smoke/snapshots/fires_in_full.json
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"falsePos": [],
|
||||||
|
"fires": [
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"_str": "a0000000000000000000000a"
|
||||||
|
},
|
||||||
|
"lat": 40.41,
|
||||||
|
"lon": -3.7,
|
||||||
|
"scan": 0.5,
|
||||||
|
"track": 0.4,
|
||||||
|
"type": "viirs",
|
||||||
|
"when": "2024-01-01T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"_str": "b0000000000000000000000b"
|
||||||
|
},
|
||||||
|
"lat": 40.43,
|
||||||
|
"lon": -3.68,
|
||||||
|
"scan": 0.5,
|
||||||
|
"track": 0.4,
|
||||||
|
"type": "viirs",
|
||||||
|
"when": "2024-01-02T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"industries": [],
|
||||||
|
"real": 2,
|
||||||
|
"total": 2
|
||||||
|
}
|
||||||
9
smoke/snapshots/mobile_falsepositive_post.json
Normal file
9
smoke/snapshots/mobile_falsepositive_post.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"upsert": {
|
||||||
|
"insertedId": "<insertedId>",
|
||||||
|
"numberAffected": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
29
smoke/snapshots/mobile_subscriptions_all.json
Normal file
29
smoke/snapshots/mobile_subscriptions_all.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"count": 1,
|
||||||
|
"subscriptions": [
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"_str": "d0000000000000000000000d"
|
||||||
|
},
|
||||||
|
"createdAt": "<createdAt>",
|
||||||
|
"distance": 40,
|
||||||
|
"geo": {
|
||||||
|
"coordinates": [
|
||||||
|
-3.7,
|
||||||
|
40.41
|
||||||
|
],
|
||||||
|
"type": "Point"
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"lat": 40.41,
|
||||||
|
"lon": -3.7
|
||||||
|
},
|
||||||
|
"owner": "<owner>",
|
||||||
|
"type": "mobile",
|
||||||
|
"updatedAt": "<updatedAt>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
4
smoke/snapshots/mobile_subscriptions_delete.json
Normal file
4
smoke/snapshots/mobile_subscriptions_delete.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"data": {},
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
6
smoke/snapshots/mobile_subscriptions_post.json
Normal file
6
smoke/snapshots/mobile_subscriptions_post.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"subsId": "d0000000000000000000000d"
|
||||||
|
},
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
13
smoke/snapshots/mobile_users_post.json
Normal file
13
smoke/snapshots/mobile_users_post.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"lang": "es",
|
||||||
|
"mobileToken": "smoke-mobile-token",
|
||||||
|
"upsertResult": {
|
||||||
|
"insertedId": "<insertedId>",
|
||||||
|
"numberAffected": 1
|
||||||
|
},
|
||||||
|
"userId": "<userId>",
|
||||||
|
"username": "<username>"
|
||||||
|
},
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
3
smoke/snapshots/status_active_fires_count.json
Normal file
3
smoke/snapshots/status_active_fires_count.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"total": 2
|
||||||
|
}
|
||||||
9
smoke/snapshots/status_last_fire_check.json
Normal file
9
smoke/snapshots/status_last_fire_check.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"_str": "51e00000000000000000000c"
|
||||||
|
},
|
||||||
|
"createdAt": "2024-01-01T00:00:00.000Z",
|
||||||
|
"name": "last-fire-check",
|
||||||
|
"updatedAt": "2024-01-01T00:00:00.000Z",
|
||||||
|
"value": "2024-01-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
29
smoke/snapshots/status_last_fire_detected.json
Normal file
29
smoke/snapshots/status_last_fire_detected.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"_str": "b0000000000000000000000b"
|
||||||
|
},
|
||||||
|
"acq_date": "2024-01-01",
|
||||||
|
"acq_time": "00:05",
|
||||||
|
"bright_ti4": 331,
|
||||||
|
"bright_ti5": 292,
|
||||||
|
"confidence": 60,
|
||||||
|
"createdAt": "2024-01-01T00:00:00.000Z",
|
||||||
|
"daynight": "N",
|
||||||
|
"frp": 6.1,
|
||||||
|
"lat": 40.43,
|
||||||
|
"lon": -3.68,
|
||||||
|
"ourid": {
|
||||||
|
"coordinates": [
|
||||||
|
-3.68,
|
||||||
|
40.43
|
||||||
|
],
|
||||||
|
"type": "Point"
|
||||||
|
},
|
||||||
|
"satellite": "N",
|
||||||
|
"scan": 0.5,
|
||||||
|
"track": 0.4,
|
||||||
|
"type": "viirs",
|
||||||
|
"updatedAt": "2024-01-01T00:00:00.000Z",
|
||||||
|
"version": "1.0NRT",
|
||||||
|
"when": "2024-01-02T00:00:00.000Z"
|
||||||
|
}
|
||||||
23
smoke/snapshots/status_subs_public_union.json
Normal file
23
smoke/snapshots/status_subs_public_union.json
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"bounds": {
|
||||||
|
"_id": {
|
||||||
|
"_str": "51e0000000000000000000b2"
|
||||||
|
},
|
||||||
|
"createdAt": "2024-01-01T00:00:00.000Z",
|
||||||
|
"name": "subs-public-union-bounds",
|
||||||
|
"updatedAt": "2024-01-01T00:00:00.000Z",
|
||||||
|
"value": "{\"ne\":{\"lat\":41.41,\"lon\":-2.7},\"sw\":{\"lat\":39.41,\"lon\":-4.7}}"
|
||||||
|
},
|
||||||
|
"union": {
|
||||||
|
"_id": {
|
||||||
|
"_str": "51e0000000000000000000a1"
|
||||||
|
},
|
||||||
|
"createdAt": "2024-01-01T00:00:00.000Z",
|
||||||
|
"name": "subs-public-union",
|
||||||
|
"updatedAt": "2024-01-01T00:00:00.000Z",
|
||||||
|
"value": "{\"type\":\"Feature\",\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[-4.7,39.41],[-2.7,39.41],[-2.7,41.41],[-4.7,41.41],[-4.7,39.41]]]]}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
3
smoke/snapshots/status_uptime.json
Normal file
3
smoke/snapshots/status_uptime.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"ms": "<ms>"
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue