todos-contra-el-fuego-web/test/server/publications.test.js
vjrj e61efc681c test(publications): cover the geo publications the map calls on every pan
activefiresmyloc (bounding box, published fields, out-of-range coordinates),
activefiresunionmyloc (geoIntersects, including a shape larger than the viewport)
and mysubscriptions (own subscriptions only, nothing for anonymous visitors).
2026-08-01 18:02:20 +02:00

200 lines
7.2 KiB
JavaScript

/* eslint-env mocha */
/* eslint-disable func-names, prefer-arrow-callback */
/* eslint-disable import/no-absolute-path */
import { expect } from 'chai';
import { Meteor } from 'meteor/meteor';
import ActiveFires from '/imports/api/ActiveFires/ActiveFires';
import ActiveFiresUnion from '/imports/api/ActiveFiresUnion/ActiveFiresUnion';
import Subscriptions from '/imports/api/Subscriptions/Subscriptions';
import '/imports/api/ActiveFires/server/publications';
import '/imports/api/ActiveFiresUnion/server/publications';
import '/imports/api/Subscriptions/server/publications';
// The map calls these three on every pan and zoom. They are also the publications
// nobody could change with any confidence, because a wrong bounding box just
// returns fewer fires and looks like "no fires around here".
const handler = name => Meteor.server.publish_handlers[name];
const runPublication = async (name, context, ...args) => {
const result = await handler(name).apply(context, args);
return result;
};
const fireAt = (lat, lon) => ({
ourid: { type: 'Point', coordinates: [lon, lat] },
lat,
lon,
type: 'viirs',
when: new Date('2026-07-30T05:30:00Z'),
scan: 0.5,
track: 0.4
});
// A small square around (lat, lon).
const squareAt = (lat, lon, size = 0.01) => ({
type: 'Polygon',
coordinates: [[
[lon - size, lat - size],
[lon + size, lat - size],
[lon + size, lat + size],
[lon - size, lat + size],
[lon - size, lat - size]
]]
});
const OVIEDO = { lat: 43.36, lon: -5.84 };
const MADRID = { lat: 40.41, lon: -3.7 };
// north-east lng/lat, south-west lng/lat, as the client sends them.
const boxAround = (p, pad = 0.5) => [p.lon + pad, p.lat + pad, p.lon - pad, p.lat - pad];
describe('geo publications', function () {
this.timeout(30000);
beforeEach(async function () {
await ActiveFires.removeAsync({});
await ActiveFiresUnion.removeAsync({});
await Subscriptions.removeAsync({});
});
after(async function () {
await ActiveFires.removeAsync({});
await ActiveFiresUnion.removeAsync({});
await Subscriptions.removeAsync({});
});
describe('activefiresmyloc', function () {
it('returns only the fires inside the requested box', async function () {
await ActiveFires.insertAsync(fireAt(OVIEDO.lat, OVIEDO.lon));
await ActiveFires.insertAsync(fireAt(MADRID.lat, MADRID.lon));
const cursor = await runPublication('activefiresmyloc', { userId: null }, ...boxAround(OVIEDO), false);
const fires = await cursor.fetchAsync();
expect(fires).to.have.lengthOf(1);
expect(fires[0].lat).to.equal(OVIEDO.lat);
});
it('publishes only the fields the map needs', async function () {
await ActiveFires.insertAsync(fireAt(OVIEDO.lat, OVIEDO.lon));
const cursor = await runPublication('activefiresmyloc', { userId: null }, ...boxAround(OVIEDO), false);
const [fire] = await cursor.fetchAsync();
expect(Object.keys(fire).sort()).to.deep.equal(['_id', 'lat', 'lon', 'scan', 'track', 'when']);
});
it('returns nothing for a box with no fires in it', async function () {
await ActiveFires.insertAsync(fireAt(MADRID.lat, MADRID.lon));
const cursor = await runPublication('activefiresmyloc', { userId: null }, ...boxAround(OVIEDO), false);
expect(await cursor.countAsync()).to.equal(0);
});
it('refuses coordinates outside the world', async function () {
let threw = false;
try {
await runPublication('activefiresmyloc', { userId: null }, 200, 10, -200, -10, false);
} catch (e) {
threw = true;
}
expect(threw, 'a longitude of 200 was accepted').to.equal(true);
});
it('refuses a non-boolean withMarks', async function () {
let threw = false;
try {
await runPublication('activefiresmyloc', { userId: null }, ...boxAround(OVIEDO), 'yes');
} catch (e) {
threw = true;
}
expect(threw).to.equal(true);
});
});
describe('activefiresunionmyloc', function () {
it('returns the union shapes that intersect the box', async function () {
await ActiveFiresUnion.insertAsync({
centerid: { type: 'Point', coordinates: [OVIEDO.lon, OVIEDO.lat] },
shape: squareAt(OVIEDO.lat, OVIEDO.lon),
when: new Date('2026-07-30T05:30:00Z')
});
await ActiveFiresUnion.insertAsync({
centerid: { type: 'Point', coordinates: [MADRID.lon, MADRID.lat] },
shape: squareAt(MADRID.lat, MADRID.lon),
when: new Date('2026-07-30T05:30:00Z')
});
const cursor = await runPublication('activefiresunionmyloc', { userId: null }, ...boxAround(OVIEDO), false);
const unions = await cursor.fetchAsync();
expect(unions).to.have.lengthOf(1);
expect(unions[0].centerid.coordinates[0]).to.equal(OVIEDO.lon);
});
// $geoIntersects, not $geoWithin: a shape bigger than the viewport still has
// to be drawn, which is exactly what a zoomed-in user sees.
it('returns a shape that only partially overlaps the box', async function () {
await ActiveFiresUnion.insertAsync({
centerid: { type: 'Point', coordinates: [OVIEDO.lon, OVIEDO.lat] },
shape: squareAt(OVIEDO.lat, OVIEDO.lon, 2),
when: new Date('2026-07-30T05:30:00Z')
});
const cursor = await runPublication('activefiresunionmyloc', { userId: null }, ...boxAround(OVIEDO, 0.1), false);
expect(await cursor.countAsync()).to.equal(1);
});
it('returns nothing for a box far from every shape', async function () {
await ActiveFiresUnion.insertAsync({
centerid: { type: 'Point', coordinates: [MADRID.lon, MADRID.lat] },
shape: squareAt(MADRID.lat, MADRID.lon),
when: new Date('2026-07-30T05:30:00Z')
});
const cursor = await runPublication('activefiresunionmyloc', { userId: null }, ...boxAround(OVIEDO), false);
expect(await cursor.countAsync()).to.equal(0);
});
it('refuses coordinates outside the world', async function () {
let threw = false;
try {
await runPublication('activefiresunionmyloc', { userId: null }, 10, 100, -10, -100, false);
} catch (e) {
threw = true;
}
expect(threw).to.equal(true);
});
});
describe('mysubscriptions', function () {
const subFor = (owner, lat, lon) => ({
location: { lat, lon },
geo: { type: 'Point', coordinates: [lon, lat] },
distance: 10,
owner,
type: 'web'
});
it('publishes only the subscriptions of the logged-in user', async function () {
await Subscriptions.insertAsync(subFor('user-a', OVIEDO.lat, OVIEDO.lon));
await Subscriptions.insertAsync(subFor('user-a', MADRID.lat, MADRID.lon));
await Subscriptions.insertAsync(subFor('user-b', MADRID.lat, MADRID.lon));
const cursor = await runPublication('mysubscriptions', { userId: 'user-a' });
const subs = await cursor.fetchAsync();
expect(subs).to.have.lengthOf(2);
subs.forEach(s => expect(s.owner).to.equal('user-a'));
});
it('publishes nothing to an anonymous visitor', async function () {
await Subscriptions.insertAsync(subFor('user-a', OVIEDO.lat, OVIEDO.lon));
const cursor = await runPublication('mysubscriptions', { userId: null });
expect(await cursor.countAsync()).to.equal(0);
});
});
});