/* eslint-env mocha */ /* eslint-disable func-names, prefer-arrow-callback */ /* eslint-disable import/no-absolute-path */ import { expect } from 'chai'; import limitUnionSize, { byteLength, MAX_UNION_BYTES } from '/imports/startup/server/unionSizeGuard'; // A closed ring of `points` vertices around (lon, lat). Coordinates are left at // full double precision on purpose: that is what turf hands back, and it is what // makes the JSON big. const ring = (lon, lat, points, radius = 0.2) => { const out = []; for (let i = 0; i < points; i += 1) { const angle = (2 * Math.PI * i) / points; out.push([lon + (radius * Math.cos(angle)), lat + (radius * Math.sin(angle))]); } out.push(out[0]); return out; }; const multiPolygon = (count, points) => { const coordinates = []; for (let i = 0; i < count; i += 1) { coordinates.push([ring(-10 + ((i % 100) * 0.5), 35 + (Math.floor(i / 100) * 0.5), points)]); } return { type: 'Feature', properties: {}, geometry: { type: 'MultiPolygon', coordinates } }; }; const polygonsOf = u => u.geometry.coordinates; const pointCount = u => polygonsOf(u).reduce((acc, poly) => acc + poly.reduce((a, r) => a + r.length, 0), 0); describe('unionSizeGuard', function () { // Building and serializing a 20 MB fixture takes a couple of seconds. this.timeout(120000); it('leaves a union that already fits completely alone', function () { const union = multiPolygon(2, 20); const result = limitUnionSize(union, MAX_UNION_BYTES); expect(result.degraded).to.equal(null); expect(result.union).to.deep.equal(union); expect(result.json).to.equal(JSON.stringify(union)); }); it('handles a null union (no valid subscriptions) without inventing geometry', function () { const result = limitUnionSize(null, MAX_UNION_BYTES); expect(result.union).to.equal(null); expect(result.json).to.equal('null'); expect(result.degraded).to.equal(null); }); it('drops coordinate precision first, keeping every polygon', function () { const union = multiPolygon(40, 60); const budget = byteLength(JSON.stringify(union)) - 1000; const result = limitUnionSize(union, budget); expect(result.bytes).to.be.at.most(budget); expect(result.degraded.steps[0]).to.match(/^precision:/); expect(polygonsOf(result.union)).to.have.lengthOf(40); }); it('decimates vertices when rounding is not enough, still keeping every polygon', function () { const union = multiPolygon(40, 144); const budget = Math.round(byteLength(JSON.stringify(union)) / 4); const result = limitUnionSize(union, budget); expect(result.bytes).to.be.at.most(budget); expect(result.degraded.steps.some(s => s.startsWith('decimate:'))).to.equal(true); expect(polygonsOf(result.union)).to.have.lengthOf(40); expect(pointCount(result.union)).to.be.below(pointCount(union)); }); it('drops the smallest polygons last, and only when nothing else fits', function () { const union = multiPolygon(60, 20); // A budget only a couple of polygons can fit into. const budget = Math.round(byteLength(JSON.stringify(union)) / 25); const result = limitUnionSize(union, budget); expect(result.bytes).to.be.at.most(budget); expect(result.degraded.steps[result.degraded.steps.length - 1]).to.match(/^polygons:/); expect(polygonsOf(result.union).length).to.be.below(60); expect(polygonsOf(result.union).length).to.be.at.least(1); }); it('keeps every ring closed and valid after degrading', function () { const union = multiPolygon(30, 144); const result = limitUnionSize(union, Math.round(byteLength(JSON.stringify(union)) / 6)); polygonsOf(result.union).forEach((polygon) => { polygon.forEach((r) => { expect(r.length).to.be.at.least(4); expect(r[0]).to.deep.equal(r[r.length - 1]); }); }); }); // The actual FIXME this module exists for: subsUnion.js used to hand Mongo // whatever came out of turf, and a document over 16 MiB is simply rejected — // the union would then stay frozen at the last one that happened to fit. it('brings a union past Mongo 16 MiB document limit back under the cap', function () { // ~20 MB of GeoJSON: 4000 subscription-sized circles at 144 steps each. const union = multiPolygon(4000, 144); const original = byteLength(JSON.stringify(union)); expect(original).to.be.above(16 * 1024 * 1024); const result = limitUnionSize(union, MAX_UNION_BYTES); expect(result.bytes).to.be.at.most(MAX_UNION_BYTES); expect(byteLength(result.json)).to.equal(result.bytes); expect(result.degraded.originalBytes).to.equal(original); expect(result.union).to.not.equal(null); // Still parseable GeoJSON, not a truncated string. expect(JSON.parse(result.json).geometry.type).to.equal('MultiPolygon'); }); it('never returns something bigger than the cap, whatever the input', function () { const budget = 2000; const result = limitUnionSize(multiPolygon(500, 144), budget); expect(result.bytes).to.be.at.most(budget); }); });