A token lasts 30 days and only a browser login yields one — the account is federated, so the server cannot mint it. Replacing it meant editing .env and recreating the container, every month. `schulcloud token set` (a hidden prompt, or piped input) and a /token page both send it to PUT /api/token. The server checks it with Schulcloud first — well-formed, unexpired, still logged in, the same account — then swaps it into the config every request reads, restarts the keepalive and saves it in STATE_DIR, a new volume, with mode 0600. At startup the newer of the saved token and TSC_JWT_COOKIE wins, unless they belong to different accounts. A refused paste changes nothing, and the token is never logged. The keepalive's pings carry a generation, so a 401 for the old token that arrives after a swap cannot stop the new cycle. `schulcloud token`, whoami and the log report the expiry and warn a week ahead. Found on the way: a host that is off for more than two hours loses the session however long the token has left — this machine lost it overnight — which is what the always-on Pi is for. 174 tests. Smoke 72/72 on the local instance, and a real swap verified end to end there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
46 lines
1.4 KiB
TypeScript
46 lines
1.4 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { afterEach, describe, it } from 'node:test';
|
|
import { loadConfig } from '../src/config.ts';
|
|
|
|
const SAVED = { ...process.env };
|
|
afterEach(() => {
|
|
process.env = { ...SAVED };
|
|
});
|
|
|
|
describe('loadConfig', () => {
|
|
it('requires the instance URL and token', () => {
|
|
delete process.env.TSC_URL;
|
|
process.env.TSC_JWT_COOKIE = 'x';
|
|
assert.throws(() => loadConfig(), /TSC_URL/);
|
|
});
|
|
|
|
it('strips trailing slashes so paths concatenate cleanly', () => {
|
|
process.env.TSC_URL = 'https://example.org///';
|
|
process.env.TSC_JWT_COOKIE = 'x';
|
|
assert.equal(loadConfig().baseUrl, 'https://example.org');
|
|
});
|
|
|
|
it('rejects a non-numeric port rather than silently defaulting', () => {
|
|
process.env.TSC_URL = 'https://example.org';
|
|
process.env.TSC_JWT_COOKIE = 'x';
|
|
process.env.PORT = 'not-a-number';
|
|
assert.throws(() => loadConfig(), /PORT/);
|
|
});
|
|
|
|
it('treats a blank auth token as absent', () => {
|
|
process.env.TSC_URL = 'https://example.org';
|
|
process.env.TSC_JWT_COOKIE = 'x';
|
|
process.env.MCP_AUTH_TOKEN = ' ';
|
|
assert.equal(loadConfig().authToken, undefined);
|
|
});
|
|
});
|
|
|
|
describe('loadConfig: state directory', () => {
|
|
it('resolves the state directory to an absolute path', () => {
|
|
process.env.TSC_URL = 'https://example.org';
|
|
process.env.TSC_JWT_COOKIE = 'x';
|
|
process.env.STATE_DIR = 'tmp/state';
|
|
assert.match(loadConfig().stateDir ?? '', /^\/.*\/tmp\/state$/);
|
|
});
|
|
});
|