mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
feat(system-notices): replace expiresAt with [minVersion, maxVersion) version gate
Prevents users who upgrade across multiple versions from seeing all interim notices at once. Version bounds are evaluated server-side using semver.coerce so prerelease builds compare as their base release. Range is lower-inclusive, upper-exclusive: maxVersion: '4.0.0' hides the notice once 4.0.0 ships.
This commit is contained in:
@@ -107,9 +107,11 @@ describe('GET /api/system-notices/active', () => {
|
||||
// welcome-v1 is also in the registry and matches firstLogin, so at least TEST_NOTICE is present
|
||||
const testNotice = res.body.find((n: { id: string }) => n.id === TEST_NOTICE.id);
|
||||
expect(testNotice).toBeDefined();
|
||||
// DTO should not expose conditions, publishedAt, expiresAt, priority
|
||||
// DTO should not expose conditions, publishedAt, minVersion, maxVersion, priority
|
||||
expect(testNotice.conditions).toBeUndefined();
|
||||
expect(testNotice.publishedAt).toBeUndefined();
|
||||
expect(testNotice.minVersion).toBeUndefined();
|
||||
expect(testNotice.maxVersion).toBeUndefined();
|
||||
} finally {
|
||||
const idx = SYSTEM_NOTICES.indexOf(TEST_NOTICE);
|
||||
if (idx !== -1) SYSTEM_NOTICES.splice(idx, 1);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import semver from 'semver';
|
||||
import { SYSTEM_NOTICES } from '../../../src/systemNotices/registry.js';
|
||||
|
||||
/** Collect all actionIds registered via registerNoticeAction() in client source files. */
|
||||
@@ -45,4 +46,21 @@ describe('registry integrity', () => {
|
||||
expect(() => new Date(n.publishedAt).toISOString()).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('minVersion and maxVersion are valid semver when set, and minVersion <= maxVersion when both set', () => {
|
||||
for (const n of SYSTEM_NOTICES) {
|
||||
if (n.minVersion !== undefined) {
|
||||
expect(semver.valid(n.minVersion), `notice "${n.id}" has invalid minVersion "${n.minVersion}"`).not.toBeNull();
|
||||
}
|
||||
if (n.maxVersion !== undefined) {
|
||||
expect(semver.valid(n.maxVersion), `notice "${n.id}" has invalid maxVersion "${n.maxVersion}"`).not.toBeNull();
|
||||
}
|
||||
if (n.minVersion && n.maxVersion) {
|
||||
expect(
|
||||
semver.lte(n.minVersion, n.maxVersion),
|
||||
`notice "${n.id}": minVersion ${n.minVersion} > maxVersion ${n.maxVersion}`
|
||||
).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { isNoticeVersionActive } from '../../../src/systemNotices/service.js';
|
||||
import type { SystemNotice } from '../../../src/systemNotices/types.js';
|
||||
|
||||
const base: SystemNotice = {
|
||||
id: 'test-notice',
|
||||
display: 'modal',
|
||||
severity: 'info',
|
||||
titleKey: 'k',
|
||||
bodyKey: 'k',
|
||||
dismissible: true,
|
||||
conditions: [],
|
||||
publishedAt: '2026-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
describe('isNoticeVersionActive', () => {
|
||||
it('passes when no bounds are set', () => {
|
||||
expect(isNoticeVersionActive(base, '3.5.0')).toBe(true);
|
||||
});
|
||||
|
||||
it('passes when app version equals minVersion (inclusive)', () => {
|
||||
expect(isNoticeVersionActive({ ...base, minVersion: '3.0.0' }, '3.0.0')).toBe(true);
|
||||
});
|
||||
|
||||
it('fails when app version is below minVersion', () => {
|
||||
expect(isNoticeVersionActive({ ...base, minVersion: '3.0.0' }, '2.9.9')).toBe(false);
|
||||
});
|
||||
|
||||
it('fails when app version equals maxVersion (exclusive upper bound)', () => {
|
||||
expect(isNoticeVersionActive({ ...base, maxVersion: '3.0.0' }, '3.0.0')).toBe(false);
|
||||
});
|
||||
|
||||
it('fails when app version exceeds maxVersion', () => {
|
||||
expect(isNoticeVersionActive({ ...base, maxVersion: '3.0.0' }, '3.0.1')).toBe(false);
|
||||
});
|
||||
|
||||
it('passes when app version is just below maxVersion', () => {
|
||||
expect(isNoticeVersionActive({ ...base, maxVersion: '3.0.0' }, '2.9.9')).toBe(true);
|
||||
});
|
||||
|
||||
it('passes when app version is inside [minVersion, maxVersion)', () => {
|
||||
expect(isNoticeVersionActive({ ...base, minVersion: '3.0.0', maxVersion: '3.9.9' }, '3.5.2')).toBe(true);
|
||||
});
|
||||
|
||||
it('treats prerelease app version as base release (semver.coerce)', () => {
|
||||
expect(isNoticeVersionActive({ ...base, minVersion: '3.0.0' }, '3.0.0-pre.42')).toBe(true);
|
||||
});
|
||||
|
||||
it('skips notice and returns false when minVersion is invalid semver', () => {
|
||||
expect(isNoticeVersionActive({ ...base, minVersion: 'not-semver' }, '3.0.0')).toBe(false);
|
||||
});
|
||||
|
||||
it('skips notice and returns false when maxVersion is invalid semver', () => {
|
||||
expect(isNoticeVersionActive({ ...base, maxVersion: 'garbage' }, '3.0.0')).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user