Files
korting/test/checkProducts.test.js
T
Sjoerd de Vries fb3fc62b4b feat: log failures, validate env at startup, load .env (findings 3, 4, 7)
Finding 3: fetch_error, not_found, mail_error and de onverwachte-fout
branch loggen nu naam/url plus reden naar stderr; runCheck logt na elke
run een regel met de tellingen per status, zodat een cron-run zichtbaar
is in de container-logs. De recordCheckSuccess/recordCheckFailure-condities
en alle return-waarden zijn ongewijzigd.

Finding 4: server.js controleert bij het opstarten dat BASIC_AUTH_USER,
BASIC_AUTH_PASS, SMTP_FROM en NOTIFY_EMAIL gezet zijn en stopt anders met
exit code 1 in plaats van later opake 500's te geven.

Finding 7: dotenv toegevoegd en als eerste regel in server.js geladen,
zodat het gedocumenteerde `cp .env.example .env && npm start` echt werkt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gwc3aCiyQxmCWQke8RvEz
2026-09-05 15:52:57 +02:00

122 lines
4.9 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const { checkProduct, checkAllProducts } = require('../src/checkProducts');
function makeDeps(overrides = {}) {
const failures = [];
const successes = [];
const mailCalls = [];
const deps = {
fetchAndExtractPrice: async () => ({ price: 10, method: 'meta', error: null }),
sendMail: async (payload) => { mailCalls.push(payload); },
recordCheckFailure: (id, status) => failures.push({ id, status }),
recordCheckSuccess: (id, data) => successes.push({ id, ...data }),
now: () => '2026-09-05T08:00:00.000Z',
...overrides,
};
return { deps, failures, successes, mailCalls };
}
// Failure paths log to stderr; capture it so the test output stays clean
// while still proving the logging happened.
function captureErrorLog(t) {
const logged = [];
t.mock.method(console, 'error', (...args) => { logged.push(args.join(' ')); });
return logged;
}
test('checkProduct stores baseline price without sending mail', async () => {
const { deps, successes, mailCalls } = makeDeps();
const product = { id: 1, name: 'X', url: 'https://x', last_price: null };
const result = await checkProduct(product, deps);
assert.equal(result.status, 'ok');
assert.equal(result.changed, false);
assert.equal(mailCalls.length, 0);
assert.deepEqual(successes, [{ id: 1, price: 10, checkedAt: '2026-09-05T08:00:00.000Z' }]);
});
test('checkProduct sends mail and updates price when price changed', async () => {
const { deps, successes, mailCalls } = makeDeps();
const product = { id: 2, name: 'X', url: 'https://x', last_price: 15 };
const result = await checkProduct(product, deps);
assert.equal(result.status, 'ok');
assert.equal(result.changed, true);
assert.equal(mailCalls.length, 1);
assert.deepEqual(mailCalls[0], { name: 'X', url: 'https://x', oldPrice: 15, newPrice: 10 });
assert.equal(successes.length, 1);
});
test('checkProduct does not send mail or update when price unchanged', async () => {
const { deps, successes, mailCalls } = makeDeps();
const product = { id: 3, name: 'X', url: 'https://x', last_price: 10 };
const result = await checkProduct(product, deps);
assert.equal(result.status, 'ok');
assert.equal(result.changed, false);
assert.equal(mailCalls.length, 0);
assert.equal(successes.length, 1);
});
test('checkProduct records fetch_error and does not touch price on network failure', async (t) => {
const logged = captureErrorLog(t);
const { deps, failures, successes } = makeDeps({
fetchAndExtractPrice: async () => ({ price: null, method: null, error: new Error('down') }),
});
const product = { id: 4, name: 'X', url: 'https://x', last_price: 10 };
const result = await checkProduct(product, deps);
assert.equal(result.status, 'fetch_error');
assert.deepEqual(failures, [{ id: 4, status: 'fetch_error' }]);
assert.equal(successes.length, 0);
assert.equal(logged.length, 1);
assert.match(logged[0], /fetch_error.*https:\/\/x.*down/);
});
test('checkProduct records not_found when no price could be extracted', async (t) => {
const logged = captureErrorLog(t);
const { deps, failures, successes } = makeDeps({
fetchAndExtractPrice: async () => ({ price: null, method: null, error: null }),
});
const product = { id: 5, name: 'X', url: 'https://x', last_price: 10 };
const result = await checkProduct(product, deps);
assert.equal(result.status, 'not_found');
assert.deepEqual(failures, [{ id: 5, status: 'not_found' }]);
assert.equal(successes.length, 0);
assert.equal(logged.length, 1);
assert.match(logged[0], /not_found.*https:\/\/x/);
});
test('checkProduct records mail_error and keeps old price when sending mail fails', async (t) => {
const logged = captureErrorLog(t);
const { deps, failures, successes } = makeDeps({
sendMail: async () => { throw new Error('smtp down'); },
});
const product = { id: 6, name: 'X', url: 'https://x', last_price: 15 };
const result = await checkProduct(product, deps);
assert.equal(result.status, 'mail_error');
assert.deepEqual(failures, [{ id: 6, status: 'mail_error' }]);
assert.equal(successes.length, 0);
assert.equal(logged.length, 1);
assert.match(logged[0], /mail_error.*https:\/\/x.*smtp down/);
});
test('checkAllProducts processes every product independently, isolating failures', async (t) => {
captureErrorLog(t);
const { deps } = makeDeps();
let calls = 0;
const throwingDeps = {
...deps,
fetchAndExtractPrice: async () => {
calls += 1;
if (calls === 1) throw new Error('boom');
return { price: 10, method: 'meta', error: null };
},
};
const products = [
{ id: 1, name: 'A', url: 'https://a', last_price: null },
{ id: 2, name: 'B', url: 'https://b', last_price: null },
];
const results = await checkAllProducts(products, throwingDeps);
assert.equal(results.length, 2);
assert.equal(results[0].status, 'error');
assert.equal(results[1].status, 'ok');
});