DC-035: add regression test for SelfUpdater.getLocalVersion() (DC-033 class)
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled

Adds 6 tests that catch the exact bug DC-033 fixed. Verified to actually
fail (4/6) against the pre-fix code (git show 20d280f^:self-updater.js),
proving it's a real regression test and not a placebo. Full suite: 40/40
suites, 1081/1081 tests.
This commit is contained in:
Krystie
2026-07-05 22:49:06 -07:00
parent 36c4528c7c
commit 71fd7cd58f
2 changed files with 102 additions and 1 deletions
+2 -1
View File
@@ -165,10 +165,11 @@
- **result:** Bumped package.json (1.14.8 → 1.14.9) + root VERSION to 1.14.9. Baked commit `42376e2` into dashcaddy-api/VERSION inside the tarball. Built `dashcaddy-1.14.9.tar.gz` (39MB, sha256 `9de120a6277f4169caa6740a15181a80cef1ba716006e3a5aad6e21b9d6542a3`). Published to `/var/www/get.dashcaddy.net/release/` (latest.tar.gz + versioned tarball + version.json + sha256). Backed up old release to `release.backup-20260706-052919`. Refreshed install.sh. Mirrored to dc-contabo-de → `/var/www/get2.dashcaddy.net/release/` (verified via SSH). Tarball verified to contain the DC-033 fix (extracted + grep'd self-updater.js — comment "Resolve package.json/VERSION relative to the api root, not __dirname" present). Live `get.dashcaddy.net/release/version.json` serves v1.14.9. SHA256 matches between local + served tarball. Local notify to localhost:3001 returned HTTP 403 (expected — DASHCADDY_UPDATE_ENABLED=false, intentional). Auto-update now ships the 0.0.0 fix to every host that updates from v1.14.8 → v1.14.9. - **result:** Bumped package.json (1.14.8 → 1.14.9) + root VERSION to 1.14.9. Baked commit `42376e2` into dashcaddy-api/VERSION inside the tarball. Built `dashcaddy-1.14.9.tar.gz` (39MB, sha256 `9de120a6277f4169caa6740a15181a80cef1ba716006e3a5aad6e21b9d6542a3`). Published to `/var/www/get.dashcaddy.net/release/` (latest.tar.gz + versioned tarball + version.json + sha256). Backed up old release to `release.backup-20260706-052919`. Refreshed install.sh. Mirrored to dc-contabo-de → `/var/www/get2.dashcaddy.net/release/` (verified via SSH). Tarball verified to contain the DC-033 fix (extracted + grep'd self-updater.js — comment "Resolve package.json/VERSION relative to the api root, not __dirname" present). Live `get.dashcaddy.net/release/version.json` serves v1.14.9. SHA256 matches between local + served tarball. Local notify to localhost:3001 returned HTTP 403 (expected — DASHCADDY_UPDATE_ENABLED=false, intentional). Auto-update now ships the 0.0.0 fix to every host that updates from v1.14.8 → v1.14.9.
### DC-035: Add regression test for getLocalVersion() — prevent DC-033 class from regressing ### DC-035: Add regression test for getLocalVersion() — prevent DC-033 class from regressing
- **status:** in-progress - **status:** done
- **owner:** krystie - **owner:** krystie
- **details:** DC-033 fixed the bug but nothing in the test suite would have caught it originally. The existing coverage on `self-updater.js` is sparse — no test exercises `getLocalVersion()` directly. Add `__tests__/self-updater-version.test.js` that: (1) `require('./src/docker/self-updater')` (matching what server.js does, NOT `require('./self-updater')` which resolves from cwd and loads the wrong file — that's a separate footgun, see DC-036). (2) instantiate SelfUpdater with minimal config. (3) call `getLocalVersion()`. (4) assert `version` is NOT `'0.0.0'` and is in semver shape (`/^\d+\.\d+\.\d+/`). (5) assert `commit` matches `/^[0-9a-f]{7,40}$/`. Optionally: parameterize to also exercise `require('./self-updater')` from `/app` cwd to verify the legacy root-copy contract still works. Effort: ~20 min. Pattern: matches DC-017's depth-2-routes-smoke.test.js (loads every module via the real path). - **details:** DC-033 fixed the bug but nothing in the test suite would have caught it originally. The existing coverage on `self-updater.js` is sparse — no test exercises `getLocalVersion()` directly. Add `__tests__/self-updater-version.test.js` that: (1) `require('./src/docker/self-updater')` (matching what server.js does, NOT `require('./self-updater')` which resolves from cwd and loads the wrong file — that's a separate footgun, see DC-036). (2) instantiate SelfUpdater with minimal config. (3) call `getLocalVersion()`. (4) assert `version` is NOT `'0.0.0'` and is in semver shape (`/^\d+\.\d+\.\d+/`). (5) assert `commit` matches `/^[0-9a-f]{7,40}$/`. Optionally: parameterize to also exercise `require('./self-updater')` from `/app` cwd to verify the legacy root-copy contract still works. Effort: ~20 min. Pattern: matches DC-017's depth-2-routes-smoke.test.js (loads every module via the real path).
- **impact:** Catches the exact class of bug DC-033 fixed, plus any future refactor that re-introduces the __dirname antipattern. - **impact:** Catches the exact class of bug DC-033 fixed, plus any future refactor that re-introduces the __dirname antipattern.
- **result:** Added `dashcaddy-api/__tests__/self-updater-version.test.js` (6 tests, all passing). Validates: (1) module loads + exports SelfUpdater class; (2) getLocalVersion returns an object with version+commit (not null); (3) version is NOT `'0.0.0'` (the DC-033 bug sentinel); (4) version matches `/^\d+\.\d+\.\d+/` semver; (5) commit is a 7-40 char hex SHA; (6) works regardless of how the module is required. **Verified the test actually catches the bug** by temporarily reverting self-updater.js to the pre-DC-033 code (`git show 20d280f^`) — 4 of 6 tests failed with the expected `expect.toBe('0.0.0')` and `not.toBeNull` assertion errors. After restoring the fix, full suite passes: **40 suites, 1081 tests** (was 39/1075, +6 new).
### DC-036: Delete dead `dashcaddy-api/self-updater.js` (root copy) — 0 runtime callers ### DC-036: Delete dead `dashcaddy-api/self-updater.js` (root copy) — 0 runtime callers
- **status:** done - **status:** done
@@ -0,0 +1,100 @@
/**
* Regression tests for getLocalVersion() — DC-033.
*
* The SelfUpdater's getLocalVersion() reads package.json + VERSION from the
* filesystem relative to its own __dirname. server.js loads it via
* `./src/docker/self-updater`, so __dirname inside the container is
* `/app/src/docker` — which has no package.json. The function's outer
* try/catch silently swallowed the ENOENT and returned the
* `{ version: '0.0.0', commit: null }` fallback, making every DashCaddy
* host running v1.14.x (≤ v1.14.8) appear to be at "version 0.0.0" in the
* dashboard and "always outdated" to checkForUpdate().
*
* DC-033 fixed it by walking a candidate list (api root first, __dirname
* second). DC-035 is the regression test: if anyone re-introduces the
* __dirname antipattern — or accidentally deletes the api-root package.json
* — this suite will fail loudly.
*
* Loading pattern matters: this test loads `./src/docker/self-updater` to
* match what server.js does at runtime. The legacy `./self-updater` path
* (from /app) was deleted by DC-036, so the only require() that exists
* now is the docker copy.
*/
const path = require('path');
describe('SelfUpdater.getLocalVersion() — DC-033 regression', () => {
// Resolve from a known cwd so require('./src/docker/self-updater') lands
// on the api-root copy, not some other relative-resolution target.
const API_ROOT = path.join(__dirname, '..');
let SelfUpdater;
beforeAll(() => {
// Sanity check: the file must exist at the expected path.
const target = path.join(API_ROOT, 'src', 'docker', 'self-updater.js');
expect(() => require.resolve(target)).not.toThrow();
const mod = require(path.join(API_ROOT, 'src', 'docker', 'self-updater.js'));
SelfUpdater = mod.SelfUpdater || mod.default || mod;
});
test('module loads and exports a SelfUpdater class', () => {
expect(typeof SelfUpdater).toBe('function');
expect(SelfUpdater.name).toBe('SelfUpdater');
});
describe('getLocalVersion() returns real version + commit', () => {
let result;
beforeAll(() => {
// Empty options — DEFAULTS will be used; getLocalVersion doesn't
// need config to read sibling files.
const instance = new SelfUpdater({});
result = instance.getLocalVersion();
});
test('result is an object with version + commit', () => {
expect(result).toEqual(expect.objectContaining({
version: expect.any(String),
commit: expect.any(String),
}));
});
test('version is NOT the 0.0.0 fallback (the DC-033 bug)', () => {
// If this fails, someone re-introduced the __dirname antipattern.
expect(result.version).not.toBe('0.0.0');
});
test('version is a valid semver string', () => {
// Anchored semver: MAJOR.MINOR.PATCH with optional pre-release/build.
// Reject '0.0.0' explicitly and anything without 3 numeric components.
expect(result.version).toMatch(/^\d+\.\d+\.\d+/);
const parts = result.version.split('.');
expect(parts.length).toBeGreaterThanOrEqual(3);
for (const part of parts) {
// Allow pre-release suffixes (e.g. "1-rc1") but the first 3 must be numeric.
const numeric = part.split('-')[0].split('+')[0];
expect(numeric).toMatch(/^\d+$/);
}
});
test('commit is a git SHA (7-40 hex chars), not null', () => {
expect(result.commit).not.toBeNull();
expect(result.commit).toMatch(/^[0-9a-f]{7,40}$/);
});
});
describe('candidate-path resolution survives missing sibling files', () => {
// If we shadow __dirname by requiring the module through a different
// require() chain, the function should still find package.json via its
// candidate-list fallback. This catches the case where someone refactors
// the file to a deeper subdirectory and forgets to update the candidates.
test('getLocalVersion works regardless of how the module is required', () => {
const mod = require(path.join(API_ROOT, 'src', 'docker', 'self-updater.js'));
const Cls = mod.SelfUpdater || mod.default || mod;
const result = new Cls({}).getLocalVersion();
expect(result.version).not.toBe('0.0.0');
expect(result.commit).toMatch(/^[0-9a-f]{7,40}$/);
});
});
});