import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { classifyNumberAssignment, translateAddNumberError } from '../src/webex/locations.js'; const LOC = { id: 'LOC-STORE-7311', name: 'Store 7311' }; // The exact JSON Webex returns when a freshly-purchased number is // still being provisioned by the carrier. Kept verbatim from a real // error so this test breaks loudly if Webex ever changes the payload // shape (and we can update our detection to match). const PENDING_ORDERS_BODY = '[{"errors":[{"errorType":"VERIFICATION","errorCode":"ERR.V.TRM.TMN60027","errorMessage":"Number has pending orders.","errorTitle":"NUMBER_HAS_PENDING_ORDERS"}]}]'; function fakeHttpError(status, body) { const err = new Error(`HTTP ${status} for POST /whatever\n${body}`); err.status = status; err.body = body; return err; } describe('translateAddNumberError', () => { it('rewrites NUMBER_HAS_PENDING_ORDERS into an actionable operator message', () => { const raw = fakeHttpError(400, PENDING_ORDERS_BODY); const translated = translateAddNumberError(raw, '+14045551212'); assert.notEqual(translated, raw, 'should be a new Error instance'); assert.match(translated.message, /pending order/i); assert.match(translated.message, /re-run \/finalizeStore/); assert.match(translated.message, /\+14045551212/); assert.equal(translated.status, 400); assert.equal(translated.cause, raw, 'raw error should be preserved as .cause'); }); it('returns unrelated errors unchanged (same instance)', () => { const raw = fakeHttpError(400, '{"message":"Nope","errorCode":9999}'); const result = translateAddNumberError(raw, '+14045551212'); assert.equal(result, raw); assert.doesNotMatch(result.message, /pending order/i); }); it('tolerates errors with no body property', () => { const raw = new Error('ENOTFOUND webexapis.com'); assert.equal(translateAddNumberError(raw, '+14045551212'), raw); }); }); // classifyNumberAssignment decides what to do about the number's // current state in the org. The whole point of extracting it is so // this branching is unit-testable without any HTTP; the four kinds // here map 1:1 to the four operator-visible outcomes. describe('classifyNumberAssignment', () => { // Regression: the endpoint that used to run in this step // (POST /telephony/config/locations/{id}/numbers) triggers a new // PSTN order — which is exactly wrong for AE's manual-order // workflow. The "already-here" path is the common case now and // must be a clean no-op. it("returns 'already-here' when the number is ACTIVE in this location", () => { const existing = { phoneNumber: '+14045551212', state: 'ACTIVE', location: { id: LOC.id, name: LOC.name }, }; const out = classifyNumberAssignment(existing, '+14045551212', LOC); assert.equal(out.kind, 'already-here'); assert.equal(out.existing, existing); }); it("returns 'pending' when the number is in the org but not ACTIVE yet", () => { const existing = { phoneNumber: '+14045551212', state: 'PENDING', location: { id: LOC.id, name: LOC.name }, }; const out = classifyNumberAssignment(existing, '+14045551212', LOC); assert.equal(out.kind, 'pending'); assert.match(out.message, /still processing/i); assert.match(out.message, /re-run \/finalizeStore/); }); it("returns 'wrong-location' when the number is ACTIVE in a different location", () => { const existing = { phoneNumber: '+14045551212', state: 'ACTIVE', location: { id: 'LOC-OTHER', name: 'Store 0499' }, }; const out = classifyNumberAssignment(existing, '+14045551212', LOC); assert.equal(out.kind, 'wrong-location'); assert.match(out.message, /Store 0499/); // current location shown assert.match(out.message, /Store 7311/); // target location shown assert.match(out.message, /Move it in Control Hub/); }); it('treats a missing location on the record as wrong-location (unassigned)', () => { const existing = { phoneNumber: '+14045551212', state: 'ACTIVE' }; const out = classifyNumberAssignment(existing, '+14045551212', LOC); assert.equal(out.kind, 'wrong-location'); assert.match(out.message, /\(unassigned\)/); }); it("returns 'not-in-org' when the number isn't in the inventory at all", () => { const out = classifyNumberAssignment(null, '+14045551212', LOC); assert.equal(out.kind, 'not-in-org'); assert.match(out.message, /Order it in Control Hub/); assert.match(out.message, /Store 7311/); }); });