From 71fd7cd58f7ec9f21b4f53bf33aa0f03ba4574ce Mon Sep 17 00:00:00 2001 From: Krystie Date: Sun, 5 Jul 2026 22:49:06 -0700 Subject: [PATCH] DC-035: add regression test for SelfUpdater.getLocalVersion() (DC-033 class) 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. --- BACKLOG.md | 3 +- .../__tests__/self-updater-version.test.js | 100 ++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 dashcaddy-api/__tests__/self-updater-version.test.js diff --git a/BACKLOG.md b/BACKLOG.md index a780bc8..425e117 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -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. ### DC-035: Add regression test for getLocalVersion() — prevent DC-033 class from regressing -- **status:** in-progress +- **status:** done - **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). - **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 - **status:** done diff --git a/dashcaddy-api/__tests__/self-updater-version.test.js b/dashcaddy-api/__tests__/self-updater-version.test.js new file mode 100644 index 0000000..dcb7952 --- /dev/null +++ b/dashcaddy-api/__tests__/self-updater-version.test.js @@ -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}$/); + }); + }); +}); \ No newline at end of file