← back to Rentv Adintel

test/export-filter.test.js

104 lines

'use strict';
/**
 * Tests for rights-aware export filtering (spec §28, §6.14, §6.18).
 * The module is being written concurrently by a teammate; import it
 * defensively and t.skip() every case if it isn't present yet so
 * `node --test` never hard-crashes.
 *
 * Rule under test: exports must honor suppression, do-not-contact, private
 * notes, and source rights — those rows/fields must be filtered OUT of any
 * exported dataset (§28 "Exports must honor suppression, do-not-contact,
 * private notes, source rights").
 */

const test = require('node:test');
const assert = require('node:assert');

let rights;
try {
  rights = require('../src/export/rights');
} catch (_e) {
  rights = null;
}

test('applyExportRights excludes do_not_contact contacts', (t) => {
  if (!rights || !rights.applyExportRights) return t.skip('export/rights not present yet');
  const rows = [
    { id: 1, value: 'ok@acme.com', do_not_contact: false, export_allowed: true },
    { id: 2, value: 'stop@acme.com', do_not_contact: true, export_allowed: true },
  ];
  const out = rights.applyExportRights(rows, 'contacts');
  const ids = out.map((r) => r.id);
  assert.ok(ids.includes(1));
  assert.ok(!ids.includes(2), 'do_not_contact contact must be excluded');
});

test('applyExportRights excludes export_allowed=false contacts', (t) => {
  if (!rights || !rights.applyExportRights) return t.skip('export/rights not present yet');
  const rows = [
    { id: 1, value: 'ok@acme.com', do_not_contact: false, export_allowed: true },
    { id: 3, value: 'internal@acme.com', do_not_contact: false, export_allowed: false },
  ];
  const out = rights.applyExportRights(rows, 'contacts');
  const ids = out.map((r) => r.id);
  assert.ok(ids.includes(1));
  assert.ok(!ids.includes(3), 'export_allowed=false contact must be excluded');
});

test('applyExportRights excludes private notes', (t) => {
  if (!rights || !rights.applyExportRights) return t.skip('export/rights not present yet');
  const rows = [
    { id: 10, body: 'public note', is_private: false },
    { id: 11, body: 'private note', is_private: true },
  ];
  const out = rights.applyExportRights(rows, 'notes');
  const ids = out.map((r) => r.id);
  assert.ok(ids.includes(10));
  assert.ok(!ids.includes(11), 'is_private note must be excluded from export');
});

test('applyExportRights excludes rows for a suppressed organization', (t) => {
  if (!rights || !rights.applyExportRights) return t.skip('export/rights not present yet');
  const rows = [
    { id: 'org-keep', name: 'Keep Co' },
    { id: 'org-suppressed', name: 'Suppressed Co' },
  ];
  // Suppression is driven by a suppression-request set passed in ctx (§6.14).
  // filterOrganizations matches the org's own id against suppressedOrgIds.
  const ctx = { suppressedOrgIds: new Set(['org-suppressed']) };
  const out = rights.applyExportRights(rows, 'organizations', ctx);
  const ids = out.map((r) => r.id);
  assert.ok(ids.includes('org-keep'));
  assert.ok(!ids.includes('org-suppressed'), 'suppressed organization must be excluded from export');
});

test('applyExportRights classifies internal-only creatives as link-only (bytes omitted)', (t) => {
  if (!rights || !rights.applyExportRights) return t.skip('export/rights not present yet');
  const rows = [
    { id: 30, organization_id: 'a', file_name: 'ad.png', rights_status: 'EXPORT_ALLOWED' },
    { id: 31, organization_id: 'a', file_name: 'internal.png', rights_status: 'INTERNAL_EVIDENCE_ONLY' },
  ];
  const out = rights.applyExportRights(rows, 'creative_assets', {});
  const byId = Object.fromEntries(out.map((r) => [r.id, r]));
  // Export-allowed → file bytes bundled; internal-only → metadata kept but bytes omitted.
  assert.strictEqual(byId[30]._exportClass, 'include');
  assert.strictEqual(byId[31]._exportClass, 'link_only');
  assert.notStrictEqual(byId[31]._exportClass, 'include', 'internal-only bytes must not be exported');
});

test('classifyAssetForExport never exports UNKNOWN-rights image bytes', (t) => {
  if (!rights || !rights.classifyAssetForExport) return t.skip('classifyAssetForExport not present yet');
  assert.strictEqual(rights.classifyAssetForExport({ rights_status: 'EXPORT_ALLOWED' }), 'include');
  assert.notStrictEqual(rights.classifyAssetForExport({ rights_status: 'UNKNOWN' }), 'include');
  assert.notStrictEqual(rights.classifyAssetForExport({ rights_status: 'INTERNAL_EVIDENCE_ONLY' }), 'include');
});

test('applyExportRights returns an array and does not mutate the input', (t) => {
  if (!rights || !rights.applyExportRights) return t.skip('export/rights not present yet');
  const rows = [{ id: 1, do_not_contact: false, export_allowed: true }];
  const copy = JSON.parse(JSON.stringify(rows));
  const out = rights.applyExportRights(rows, 'contacts', {});
  assert.ok(Array.isArray(out));
  assert.deepStrictEqual(rows, copy, 'input rows must not be mutated');
});