import assert from 'node:assert/strict'; import { inspect } from 'node:util'; import { payableCorrect, payableFaulty } from './discount.mjs'; const implementation = process.argv[2] ?? 'correct'; if (!['correct', 'faulty'].includes(implementation)) { console.error('Usage: node test-discount.mjs correct|faulty'); process.exit(2); } const calculate = implementation === 'correct' ? payableCorrect : payableFaulty; let passed = 0; let failed = 0; function check(name, work) { try { const details = work(); passed++; console.log(`PASS ${name}${details ? ' | ' + details : ''}`); } catch (error) { failed++; console.log(`FAIL ${name} | ${error.message.replaceAll('\n', ' ')}`); } } // Fixed expected results were calculated separately, not by calling the // implementation under test. Comments retain the exact arithmetic. const examples = [ ['ordinary exact', 1200, 20, 960], // 1200 * 80/100 = 960 ['ordinary fraction', 1999, 15, 1699], // 1999 * 85/100 = 1699.15 ['half-unit boundary', 101, 50, 51], // 101 * 50/100 = 50.5 -> 51 ['below half-unit', 1, 51, 0], // 1 * 49/100 = 0.49 -> 0 ['exact half-unit', 1, 50, 1], // 1 * 50/100 = 0.50 -> 1 ['above half-unit', 1, 49, 1], // 1 * 51/100 = 0.51 -> 1 ['zero amount', 0, 25, 0], ['zero discount', 101, 0, 101], ['full discount', 101, 100, 0], ['maximum amount', 1_000_000_000, 0, 1_000_000_000], ['maximum amount discounted', 1_000_000_000, 99, 10_000_000], ['maximum amount full discount', 1_000_000_000, 100, 0], ]; for (const [name, amount, rate, expected] of examples) { check(name, () => { const actual = calculate(amount, rate); assert.equal(actual, expected, `amount=${amount} rate=${rate} expected=${expected} actual=${actual}`); return `amount=${amount} rate=${rate} expected=${expected} actual=${actual}`; }); } const invalid = [ ['negative amount', -1, 20, RangeError], ['amount over cap', 1_000_000_001, 20, RangeError], ['fractional amount', 1.5, 20, TypeError], ['blank amount', '', 20, TypeError], ['numeric string amount', '101', 20, TypeError], ['null amount', null, 20, TypeError], ['missing amount', undefined, 20, TypeError], ['boolean amount', true, 20, TypeError], ['array amount', [], 20, TypeError], ['object amount', {}, 20, TypeError], ['NaN amount', NaN, 20, TypeError], ['infinite amount', Infinity, 20, TypeError], ['unsafe integer amount', Number.MAX_SAFE_INTEGER + 1, 20, TypeError], ['negative rate', 101, -1, RangeError], ['rate over cap', 101, 101, RangeError], ['fractional rate', 101, 12.5, TypeError], ['blank rate', 101, '', TypeError], ['numeric string rate', 101, '50', TypeError], ['missing rate', 101, undefined, TypeError], ['NaN rate', 101, NaN, TypeError], ['infinite rate', 101, Infinity, TypeError], ]; for (const [name, amount, rate, ErrorType] of invalid) { check(name, () => { assert.throws(() => calculate(amount, rate), ErrorType); return `amount=${inspect(amount)} rate=${inspect(rate)} throws ${ErrorType.name}`; }); } check('repeated calculation A B A', () => { const actual = [calculate(1200, 20), calculate(101, 50), calculate(1200, 20)]; assert.deepEqual(actual, [960, 51, 960], `expected=[960,51,960] actual=${JSON.stringify(actual)}`); return `expected=[960,51,960] actual=${JSON.stringify(actual)}`; }); check('valid calculation after invalid input', () => { assert.throws(() => calculate('', 20), TypeError); const actual = calculate(1200, 20); assert.equal(actual, 960); return `rejected blank amount, then expected=960 actual=${actual}`; }); console.log(`SUMMARY implementation=${implementation} passed=${passed} failed=${failed} total=${passed + failed}`); process.exitCode = failed ? 1 : 0;