mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
feat(server): TREK_S3_* env wiring — schema, s3 namespace, all-or-nothing boot rule
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
deriveBackup,
|
||||
deriveNet,
|
||||
derivePaths,
|
||||
deriveS3,
|
||||
deriveAll,
|
||||
} from '../../../src/app-config/derive';
|
||||
|
||||
@@ -233,6 +234,28 @@ describe('derivePaths', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('deriveS3', () => {
|
||||
const full = {
|
||||
TREK_S3_ENDPOINT: 'http://127.0.0.1:9000',
|
||||
TREK_S3_BUCKET: 'trek',
|
||||
TREK_S3_ACCESS_KEY_ID: 'ak',
|
||||
TREK_S3_SECRET_ACCESS_KEY: 'sk',
|
||||
};
|
||||
it('is unconfigured when nothing is set, with defaults intact', () => {
|
||||
const s3 = deriveS3({});
|
||||
expect(s3.configured).toBe(false);
|
||||
expect(s3).toMatchObject({ region: 'us-east-1', keyPrefix: '', retries: 1, timeoutMs: 30000 });
|
||||
});
|
||||
it('is configured only when all four required vars are non-blank', () => {
|
||||
expect(deriveS3(full).configured).toBe(true);
|
||||
expect(deriveS3({ ...full, TREK_S3_BUCKET: ' ' }).configured).toBe(false);
|
||||
});
|
||||
it('applies overrides and trims values', () => {
|
||||
const s3 = deriveS3({ ...full, TREK_S3_REGION: ' auto ', TREK_S3_KEY_PREFIX: 'trek/prod', TREK_S3_RETRIES: '0', TREK_S3_TIMEOUT_MS: '5000' });
|
||||
expect(s3).toMatchObject({ region: 'auto', keyPrefix: 'trek/prod', retries: 0, timeoutMs: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('deriveAll', () => {
|
||||
it('assembles every namespace', () => {
|
||||
const env = deriveAll({ PORT: '4000', DEMO_MODE: 'true' });
|
||||
@@ -240,7 +263,7 @@ describe('deriveAll', () => {
|
||||
expect(env.demo.enabled).toBe(true);
|
||||
for (const ns of [
|
||||
'app', 'http', 'session', 'demo', 'adminBootstrap', 'oidc', 'smtp', 'mcp',
|
||||
'plugins', 'webauthn', 'integrations', 'backup', 'db', 'paths', 'net',
|
||||
'plugins', 'webauthn', 'integrations', 'backup', 'db', 'paths', 'net', 's3',
|
||||
] as const) {
|
||||
expect(env[ns]).toBeDefined();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
csvList,
|
||||
csvListFiltered,
|
||||
nonNegativeIntOr,
|
||||
numberOr,
|
||||
parseBool,
|
||||
parseDurationMs,
|
||||
@@ -80,6 +81,19 @@ describe('positiveIntOr', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('nonNegativeIntOr', () => {
|
||||
it('accepts zero (0 disables retries) and positive integers', () => {
|
||||
expect(nonNegativeIntOr('0', 1)).toBe(0);
|
||||
expect(nonNegativeIntOr('3', 1)).toBe(3);
|
||||
});
|
||||
it('falls back on unset, blank, negative, and non-numeric', () => {
|
||||
expect(nonNegativeIntOr(undefined, 1)).toBe(1);
|
||||
expect(nonNegativeIntOr(' ', 1)).toBe(1);
|
||||
expect(nonNegativeIntOr('-1', 1)).toBe(1);
|
||||
expect(nonNegativeIntOr('nope', 1)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('csvList / csvListFiltered', () => {
|
||||
it('returns null when unset or empty', () => {
|
||||
expect(csvList(undefined)).toBeNull();
|
||||
|
||||
@@ -156,6 +156,45 @@ describe('validateEnvAtBoot — centrally administered preconditions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateEnvAtBoot — S3 all-or-nothing preconditions', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const full = {
|
||||
TREK_S3_ENDPOINT: 'http://127.0.0.1:9000',
|
||||
TREK_S3_BUCKET: 'trek',
|
||||
TREK_S3_ACCESS_KEY_ID: 'ak',
|
||||
TREK_S3_SECRET_ACCESS_KEY: 'sk',
|
||||
};
|
||||
it('S3-BOOT-001: refuses a partial TREK_S3_* set', () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
expect(() => validateEnvAtBoot({ TREK_S3_ENDPOINT: full.TREK_S3_ENDPOINT })).toThrow(
|
||||
/Invalid environment configuration \(3 problems\)/,
|
||||
);
|
||||
});
|
||||
it('S3-BOOT-002: names each missing variable and the why', () => {
|
||||
const error = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
expect(() => validateEnvAtBoot({ TREK_S3_REGION: 'auto' })).toThrow();
|
||||
const report = error.mock.calls.map((c) => c.join(' ')).join('\n');
|
||||
expect(report).toContain('TREK_S3_ENDPOINT is unset');
|
||||
expect(report).toContain('TREK_S3_SECRET_ACCESS_KEY is unset');
|
||||
expect(report).toContain('required when any TREK_S3_');
|
||||
});
|
||||
it('S3-BOOT-003: passes with the full required set', () => {
|
||||
expect(() => validateEnvAtBoot(full)).not.toThrow();
|
||||
});
|
||||
it('S3-BOOT-004: inert when no TREK_S3_* variable is set', () => {
|
||||
expect(() => validateEnvAtBoot({})).not.toThrow();
|
||||
});
|
||||
it('S3-BOOT-005: rejects a malformed key prefix alongside schema problems', () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
expect(() => validateEnvAtBoot({ ...full, TREK_S3_KEY_PREFIX: '../evil', TREK_S3_ENDPOINT: 'not a url' })).toThrow(
|
||||
/Invalid environment configuration \(2 problems\)/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readEnv', () => {
|
||||
it('reads process.env live — a runtime mutation is visible on the next call', () => {
|
||||
const before = process.env.DEMO_MODE;
|
||||
|
||||
Reference in New Issue
Block a user