// ── The editor that must not touch what it was not asked to ─────────────── // // `configEdit.js` exists for one reason: a config file goes back to the game // host byte-identical except where an admin deliberately changed something. So // the suite is mostly about what does NOT change, and the first test is the one // the whole design is for. // // It is worth being concrete about the failure being prevented. `Rate: 1.0` in // an untouched field, read through `JSON.parse` and written back through // `JSON.stringify`, becomes `Rate: 1`. Newtonsoft may coerce that into a // `float` or may throw; if it throws, the plugin does not come back from its // reload — and R6/R17 make four plugins required, so "ZoneManager is down" is // also "event participation is down". const test = require('node:test') const assert = require('node:assert') const configEdit = require('../configEdit') /** A config with every shape that has ever caused trouble. */ const SAMPLE = `{ "Gather": { "Wood": 1.0, "Stone": 2.50, "Sulfur": 3, "Scale": 1e3 }, "Enabled": true, "Message": "Welcome, {name}", "Discord Webhook": "https://discord.com/api/webhooks/1/abc", "Zones": ["a", "b"], "Nothing": null, "Empty": [] }` test('an untouched float keeps its literal — the whole point of this file', () => { const { text, changes } = configEdit.applyEdits(SAMPLE, [{ pointer: ['Enabled'], value: false }]) assert.equal(changes.length, 1) assert.match(text, /"Wood": 1\.0/) assert.match(text, /"Stone": 2\.50/) assert.match(text, /"Scale": 1e3/) assert.match(text, /"Enabled": false/) // And the proof that the naive implementation would have failed this: the same // document through parse/stringify loses all three. const naive = JSON.stringify(JSON.parse(SAMPLE)) assert.match(naive, /"Wood":1,/) assert.doesNotMatch(naive, /2\.50/) assert.doesNotMatch(naive, /1e3/) }) test('a number is written as the literal an admin typed, not as a Number', () => { const { text } = configEdit.applyEdits(SAMPLE, [{ pointer: ['Gather', 'Wood'], raw: '2.0' }]) assert.match(text, /"Wood": 2\.0/) // The same edit through a JavaScript number would have produced `2`, which is // a different C# type at the far end. assert.equal(String(2.0), '2') }) test('everything else in the document is byte-identical', () => { const { text } = configEdit.applyEdits(SAMPLE, [{ pointer: ['Gather', 'Sulfur'], raw: '4' }]) const before = SAMPLE.split('\n') const after = text.split('\n') assert.equal(before.length, after.length) before.forEach((line, i) => { if (line.includes('"Sulfur"')) return assert.equal(after[i], line, `line ${i + 1} changed and should not have`) }) }) test('a literal that is not a JSON number is refused', () => { for (const raw of ['0x10', '', ' ', '1.', '.5', '01', 'NaN', 'Infinity', '1,0', '5; rm -rf /']) { const { error, text } = configEdit.applyEdits(SAMPLE, [{ pointer: ['Gather', 'Sulfur'], raw }]) assert.ok(error, `'${raw}' should be refused`) assert.equal(text, undefined) } // And the ones that must keep working, because preserving them is the point. for (const raw of ['1.0', '-2', '1e3', '1E-3', '0', '0.5', '123456789012345678']) { const { error } = configEdit.applyEdits(SAMPLE, [{ pointer: ['Gather', 'Sulfur'], raw }]) assert.equal(error, undefined, `'${raw}' should be accepted`) } }) test('the form cannot change a value KIND — that is the raw tier', () => { assert.match( configEdit.applyEdits(SAMPLE, [{ pointer: ['Enabled'], value: 'yes' }]).error, /true or false/, ) assert.match(configEdit.applyEdits(SAMPLE, [{ pointer: ['Message'], value: 7 }]).error, /expected text/) assert.match(configEdit.applyEdits(SAMPLE, [{ pointer: ['Zones'], value: 'a' }]).error, /raw tier/) assert.match(configEdit.applyEdits(SAMPLE, [{ pointer: ['Nothing'], value: 1 }]).error, /raw tier/) }) test('a string is escaped on the way in', () => { const { text } = configEdit.applyEdits(SAMPLE, [ { pointer: ['Message'], value: 'He said "hi"\nand left\\' }, ]) assert.match(text, /"Message": "He said \\"hi\\"\\nand left\\\\"/) // Still JSON, and still the same string coming back out. assert.equal(JSON.parse(text).Message, 'He said "hi"\nand left\\') }) test('a pointer that is not in the file is refused rather than created', () => { assert.match(configEdit.applyEdits(SAMPLE, [{ pointer: ['Nope'], value: true }]).error, /not in this file/) assert.match( configEdit.applyEdits(SAMPLE, [{ pointer: ['Gather', 'Wood', 'Deeper'], raw: '1' }]).error, /not in this file/, ) }) test('an edit set that changes nothing writes nothing', () => { const { text, changes } = configEdit.applyEdits(SAMPLE, [{ pointer: ['Enabled'], value: true }]) assert.equal(text, SAMPLE) assert.deepEqual(changes, []) }) test('two edits to the same field in one save are refused', () => { const { error } = configEdit.applyEdits(SAMPLE, [ { pointer: ['Gather', 'Wood'], raw: '1.0' }, { pointer: ['Gather', 'Wood'], raw: '2.0' }, ]) assert.match(error, /edited twice/) }) test('several edits land together, and later offsets are not shifted by earlier ones', () => { const { text, changes } = configEdit.applyEdits(SAMPLE, [ { pointer: ['Gather', 'Wood'], raw: '10.0' }, { pointer: ['Message'], value: 'much longer than it was before' }, { pointer: ['Zones', 1], value: 'bb' }, ]) assert.equal(changes.length, 3) const parsed = JSON.parse(text) assert.equal(parsed.Gather.Wood, 10) assert.equal(parsed.Message, 'much longer than it was before') assert.deepEqual(parsed.Zones, ['a', 'bb']) assert.match(text, /"Wood": 10\.0/) }) test('a locked key cannot be edited, and the refusal names it (D38)', () => { const own = '{\n "Host": "127.0.0.1",\n "Port": 7799,\n "QueueCap": 5000\n}' const locked = ['Host', 'Port', 'ServerId'] assert.match(configEdit.applyEdits(own, [{ pointer: ['Port'], raw: '1' }], { locked }).error, /Port/) assert.match( configEdit.applyEdits(own, [{ pointer: ['Host'], value: '10.0.0.5' }], { locked }).error, /cannot be edited/, ) // Everything else in the bridge's own config stays editable, which is the // half of D38 that is easy to lose. const { text } = configEdit.applyEdits(own, [{ pointer: ['QueueCap'], raw: '9000' }], { locked }) assert.match(text, /"QueueCap": 9000/) }) test('a secret is flagged by WORD, not by substring', () => { for (const key of ['ApiKey', 'Discord Webhook', 'steam_api_key', 'Token', 'Password', 'authToken']) { assert.equal(configEdit.isSecretKey(key), true, `${key} should be a secret`) } // The false positives a substring match would produce, and they matter: a // form that masks a third of every config teaches an operator to ignore the // mask, which is worse than not masking. for (const key of ['Monkey', 'Keybind', 'Passive Mode', 'Authority', 'Keycards Allowed']) { assert.equal(configEdit.isSecretKey(key), false, `${key} should not be a secret`) } // A genuinely ambiguous one, resolved toward masking on purpose: a field // called `Keys` is a credential often enough, and the cost of being wrong is // a field an admin has to click to read rather than a credential on a page. assert.equal(configEdit.isSecretKey('Keys'), true) }) test("a secret's values never reach the audit trail, though the change is recorded", () => { const { changes } = configEdit.applyEdits(SAMPLE, [ { pointer: ['Discord Webhook'], value: 'https://discord.com/api/webhooks/2/def' }, ]) assert.equal(changes.length, 1) assert.equal(changes[0].path, 'Discord Webhook') assert.equal(changes[0].from, '***') assert.equal(changes[0].to, '***') assert.equal(changes[0].secret, true) }) test('the form description says which fields it cannot draw, and why', () => { const fields = configEdit.describe(configEdit.scan(SAMPLE)) const by = (path) => fields.find((f) => f.path === path) assert.equal(by('Gather.Wood').type, 'number') assert.equal(by('Gather.Wood').raw, '1.0') assert.equal(by('Enabled').type, 'boolean') assert.equal(by('Zones[0]').value, 'a') assert.equal(by('Discord Webhook').secret, true) // The three things a value cannot tell us anything about. assert.equal(by('Nothing').advanced, true) assert.equal(by('Empty').advanced, true) assert.ok(by('Nothing').reason) assert.ok(by('Empty').reason) }) test('a subtree past the depth limit is advanced-only rather than half-drawn', () => { const deep = '{"a":{"b":{"c":{"d":{"e":{"f":{"g":1}}}}}}}' const fields = configEdit.describe(configEdit.scan(deep), { maxDepth: 3 }) const past = fields.find((f) => f.path === 'a.b.c') assert.equal(past.advanced, true) assert.equal(fields.some((f) => f.path.startsWith('a.b.c.')), false) }) test('a document that is not JSON is refused with a position', () => { assert.throws(() => configEdit.scan('{"a": }'), /offset/) assert.throws(() => configEdit.scan('{"a": 1,}'), /expected a key/) assert.throws(() => configEdit.scan('{} trailing'), /trailing content/) assert.throws(() => configEdit.scan('{"a": "unterminated'), /unterminated/) const { error } = configEdit.applyEdits('{ not json', [{ pointer: ['a'], value: true }]) assert.match(error, /not valid JSON/) }) test('escapes and unicode survive a scan of a document nobody edited', () => { const text = '{"a":"tab\\there","b":"\\u00e9\\u0041","c":"slash\\/"}' const root = configEdit.scan(text) const values = Object.fromEntries(root.children.map((c) => [c.key, c.value])) assert.deepEqual(values, JSON.parse(text)) })