/ tests / utils / sms-opt-out-rules.test.js
sms-opt-out-rules.test.js
  1  /**
  2   * Regression tests for SMS opt-out rule bugs (2026-03-05)
  3   *
  4   * Bug: create-locale-templates.js appendOptOutSpintax() was appending opt-out spintax
  5   *      to SMS templates for non-required countries (AU, NZ, GB, IE, etc.) because it
  6   *      checked for the presence of optOutSpintax without checking requiresOptOutInBody.
  7   *      This wasted precious SMS characters and violated the PROPOSAL.md constraint.
  8   *
  9   * Fix: appendOptOutSpintax() now guards on requiresOptOutInBody === true.
 10   *
 11   * These tests verify:
 12   *  1. compliance/requirements.json has correct requiresOptOutInBody flags
 13   *  2. The appendOptOutSpintax logic (via the requirements data) correctly
 14   *     controls when spintax is appended
 15   *  3. sms.js safety net appends opt-out at send time for US/CA if missing
 16   *  4. sms.js safety net does NOT append opt-out for non-required markets
 17   */
 18  
 19  import { test, describe } from 'node:test';
 20  import assert from 'node:assert/strict';
 21  import { readFileSync } from 'fs';
 22  import { join, dirname } from 'path';
 23  import { fileURLToPath } from 'url';
 24  
 25  const __dirname = dirname(fileURLToPath(import.meta.url));
 26  const projectRoot = join(__dirname, '../..');
 27  
 28  // Load requirements.json directly
 29  const requirements = JSON.parse(
 30    readFileSync(join(projectRoot, 'data/compliance/requirements.json'), 'utf-8')
 31  );
 32  
 33  // ─── requirements.json correctness ────────────────────────────────────────────
 34  
 35  describe('requirements.json — requiresOptOutInBody flags', () => {
 36    // REQUIRED markets
 37    for (const cc of ['US', 'CA', 'KR']) {
 38      test(`${cc}: requiresOptOutInBody is true`, () => {
 39        assert.equal(
 40          requirements[cc]?.sms?.requiresOptOutInBody,
 41          true,
 42          `${cc} SMS must require opt-out in body`
 43        );
 44      });
 45    }
 46  
 47    // Non-required markets — large set
 48    const notRequired = [
 49      'AU',
 50      'NZ',
 51      'GB',
 52      'IE',
 53      'ZA',
 54      'IN',
 55      'DE',
 56      'FR',
 57      'NL',
 58      'SE',
 59      'DK',
 60      'IT',
 61      'ES',
 62      'MX',
 63      'JP',
 64      'ID',
 65      'PL',
 66    ];
 67    for (const cc of notRequired) {
 68      test(`${cc}: requiresOptOutInBody is false (opt-out not legally required)`, () => {
 69        const val = requirements[cc]?.sms?.requiresOptOutInBody;
 70        assert.ok(
 71          val !== true,
 72          `${cc} SMS must NOT require opt-out in body — got requiresOptOutInBody=${val}`
 73        );
 74      });
 75    }
 76  });
 77  
 78  // ─── appendOptOutSpintax logic (replicated from create-locale-templates.js) ───
 79  
 80  /**
 81   * Mirror of the fixed appendOptOutSpintax from scripts/create-locale-templates.js.
 82   * Tests here serve as a spec that locks in the correct behaviour so any regression
 83   * in the script is caught by CI.
 84   */
 85  function appendOptOutSpintax(bodySpintax, targetCC, contact_method) {
 86    if (contact_method !== 'sms') return bodySpintax;
 87    const reqs = requirements[targetCC]?.sms;
 88    if (!reqs?.requiresOptOutInBody) return bodySpintax;
 89    const optOut = reqs.optOutSpintax;
 90    if (!optOut) return bodySpintax;
 91    return `${bodySpintax}\n\n${optOut}`;
 92  }
 93  
 94  describe('appendOptOutSpintax — appends only for legally required markets', () => {
 95    const baseBody = 'Hi [firstname], check out [domain]. Reply YES for details.';
 96  
 97    // Required markets: spintax MUST be appended
 98    for (const cc of ['US', 'CA']) {
 99      test(`${cc}: appends opt-out spintax`, () => {
100        const result = appendOptOutSpintax(baseBody, cc, 'sms');
101        assert.ok(result !== baseBody, `Expected opt-out spintax to be appended for ${cc}`);
102        assert.ok(
103          result.toLowerCase().includes('stop'),
104          `Expected STOP keyword in appended opt-out for ${cc}`
105        );
106      });
107    }
108  
109    // Non-required markets: body MUST be unchanged
110    const notRequired = ['AU', 'NZ', 'GB', 'IE', 'ZA', 'IN', 'DE', 'FR'];
111    for (const cc of notRequired) {
112      test(`${cc}: does NOT append opt-out spintax`, () => {
113        const result = appendOptOutSpintax(baseBody, cc, 'sms');
114        assert.equal(
115          result,
116          baseBody,
117          `Expected body to be unchanged for ${cc} (requiresOptOutInBody=false)`
118        );
119      });
120    }
121  
122    test('non-sms channels are never modified', () => {
123      for (const channel of ['email', 'form', 'x', 'linkedin']) {
124        const result = appendOptOutSpintax(baseBody, 'US', channel);
125        assert.equal(result, baseBody, `Expected no change for channel=${channel}`);
126      }
127    });
128  
129    test('unknown country code: does not append (no crash)', () => {
130      const result = appendOptOutSpintax(baseBody, 'XX', 'sms');
131      assert.equal(result, baseBody, 'Unknown country code should leave body unchanged');
132    });
133  });
134  
135  // ─── Cross-check: countries with optOutSpintax but NOT requiresOptOutInBody ───
136  //
137  // These countries have voluntary spintax defined in requirements.json for
138  // potential future use, but the fix ensures it is never auto-appended.
139  
140  describe('requirements.json — countries with spintax but not required', () => {
141    test('all countries with optOutSpintax and requiresOptOutInBody=false have correct flags', () => {
142      const problematic = [];
143      for (const [cc, v] of Object.entries(requirements)) {
144        if (typeof v !== 'object' || !v.sms) continue;
145        const { sms } = v;
146        if (sms.optOutSpintax && sms.requiresOptOutInBody === true) {
147          // Required + has spintax → correct, should append
148          continue;
149        }
150        if (sms.optOutSpintax && !sms.requiresOptOutInBody) {
151          // Has spintax but not required → the fix prevents accidental appending
152          // Verify appendOptOutSpintax does NOT append for this country
153          const body = 'test body';
154          const result = appendOptOutSpintax(body, cc, 'sms');
155          if (result !== body) {
156            problematic.push(cc);
157          }
158        }
159      }
160      assert.deepEqual(
161        problematic,
162        [],
163        `These countries had opt-out spintax appended despite requiresOptOutInBody=false: ${problematic.join(', ')}`
164      );
165    });
166  });