Files
TREK/server/tests/unit/nest/managed-keys.test.ts
T
Maurice 86513fda0d Managed mode: hand place search, routing, plugins and docs to the operator
On a managed install the operator runs place search and routing, the plugin
runtime, the integrations and the documentation, so the admin and the users
no longer see switches and guides for them:

- The place search and routing card, the transit provider and the search log
  are gone from the admin settings (desktop and mobile). places_provider is a
  managed-locked setting, and the transit provider, search log and wanderer
  routes refuse writes there. Share place details stays the admin's, as a card
  of its own, hidden when TREK_PLACES_CONTRIBUTE switches it off.
- File types and trip reminders move to the right column, so the columns stay
  balanced.
- Plugins disappear from the admin and the user settings, wanderer from Tours.
- The Help & Docs page and its links are not offered. The help center stays and
  leaves out guides about what the operator runs (keys, OIDC, backups, storage,
  releases, plugins, document sync) and about integrations that are off.
- Mapbox GL is offered only with the operator's token, Amap tiles only where
  the operator set Amap up.
- The mobile admin no longer shows OIDC and the WebAuthn relying party fields,
  matching the desktop tab.

For every install:

- Notification matrices list only events the install can raise: no plugin row
  without the plugin runtime, no Vacay, collection, photo, Synology, wanderer or
  packing rows while those are off. On a managed install the release and
  replica alerts are left out.
- The journey entry editor offers External photos only where a photo provider
  is switched on, as the phone already did.
- app-config is asked again after a failed answer. A page opened while the
  server restarted kept every default for the session, which on a managed
  install brought back the operator's settings.
2026-11-03 08:44:45 +01:00

131 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Who owns which setting, pinned.
*
* The lists grow and the mode does not: that is how this kind of switch drifts.
* A key added to the admin form (ADMIN_FORM_SETTING_KEYS) or DEFAULTABLE_USER_SETTING_KEYS belongs to
* neither set until somebody decides, and MANAGED-KEYS-001 is what makes that
* decision unavoidable rather than optional.
*
* Deliberately an assignment test, not an equality test. Pinning the locked list
* alone would pass forever while new keys quietly defaulted to "the admin may
* set it", which is the failure this is here to prevent.
*/
import { ADMIN_FORM_SETTING_KEYS } from '../../../src/nest/common/app-settings.registry';
import {
MANAGED_LOCKED_SETTING_KEYS,
MANAGED_LOCKED_PROFILE_KEYS,
MANAGED_CUSTOMER_KEYS,
isManagedLockedKey,
splitManagedKeys,
} from '../../../src/nest/common/managed';
import { DEFAULTABLE_USER_SETTING_KEYS } from '../../../src/nest/settings/settings.service';
import { describe, it, expect } from 'vitest';
const SOURCE_KEYS: string[] = [...ADMIN_FORM_SETTING_KEYS, ...DEFAULTABLE_USER_SETTING_KEYS];
const locked = new Set<string>(MANAGED_LOCKED_SETTING_KEYS);
const customer = new Set<string>(MANAGED_CUSTOMER_KEYS);
describe('managed key assignment', () => {
it('MANAGED-KEYS-001: every key from both source lists is assigned to exactly one set', () => {
const unassigned = SOURCE_KEYS.filter((k) => !locked.has(k) && !customer.has(k));
const both = SOURCE_KEYS.filter((k) => locked.has(k) && customer.has(k));
// Both in one assertion so the failure message names the offending key.
expect({ unassigned, both }).toEqual({ unassigned: [], both: [] });
});
it('MANAGED-KEYS-002: no customer entry outlives the key it describes', () => {
expect(MANAGED_CUSTOMER_KEYS.filter((k) => !SOURCE_KEYS.includes(k))).toEqual([]);
});
it('MANAGED-KEYS-003: the only locked names outside both source lists are the users columns and the service key', () => {
// service_key is written by the server alone, never through a settings
// form; it is locked so a managed install keeps the operator's key.
expect(
MANAGED_LOCKED_SETTING_KEYS.filter((k) => !SOURCE_KEYS.includes(k))
.slice()
.sort(),
).toEqual([...MANAGED_LOCKED_PROFILE_KEYS, 'service_key'].sort());
});
it('MANAGED-KEYS-004: neither set repeats itself', () => {
expect(locked.size).toBe(MANAGED_LOCKED_SETTING_KEYS.length);
expect(customer.size).toBe(MANAGED_CUSTOMER_KEYS.length);
});
it('MANAGED-KEYS-005: the locked list is pinned verbatim', () => {
expect([...MANAGED_LOCKED_SETTING_KEYS]).toEqual([
'amap_api_key',
'carto_api_key',
'llm_api_key',
'llm_base_url',
'llm_model',
'llm_multimodal',
'llm_provider',
'mapbox_access_token',
'maps_api_key',
'oidc_login',
'oidc_registration',
'openweather_api_key',
'places_provider',
'routing_base_url',
'routing_valhalla_provider',
'service_key',
'smtp_from',
'smtp_host',
'smtp_pass',
'smtp_port',
'smtp_skip_tls_verify',
'smtp_user',
'unsplash_api_key',
'valhalla_base_url',
'webauthn_origins',
'webauthn_rp_id',
]);
});
it("MANAGED-KEYS-006: mapbox_access_token is the operator's, and reaches the browser by design", () => {
// The one locked key whose value is meant to be public: a managed instance
// ships the operator's pk.* token, injected when settings are read. Locked so
// a per-user save cannot land on top of it and break that user's map.
expect(isManagedLockedKey('mapbox_access_token')).toBe(true);
expect(customer.has('mapbox_access_token')).toBe(false);
});
});
describe('splitManagedKeys', () => {
it('MANAGED-KEYS-007: hands the body back untouched on a self-hosted install', () => {
const body = { smtp_host: 'mail.example.test', allow_registration: 'true' };
expect(splitManagedKeys(body, false)).toEqual({ allowed: body, blocked: [] });
});
it('MANAGED-KEYS-008: keeps the customer keys and reports the locked ones', () => {
// The admin settings tab saves SMTP and the registration toggles in one
// request. Refusing the request would take the toggles down with the SMTP.
const { allowed, blocked } = splitManagedKeys(
{ smtp_host: 'mail.example.test', smtp_pass: 'hunter2', allow_registration: 'true' },
true,
);
expect(allowed).toEqual({ allow_registration: 'true' });
expect(blocked).toEqual(['smtp_host', 'smtp_pass']);
});
it('MANAGED-KEYS-009: reports blocked names sorted, so a response body is stable', () => {
const { blocked } = splitManagedKeys({ webauthn_rp_id: 'x', llm_api_key: 'y', smtp_from: 'z' }, true);
expect(blocked).toEqual(['llm_api_key', 'smtp_from', 'webauthn_rp_id']);
});
it('MANAGED-KEYS-010: an unknown key is not the filter’s business', () => {
// Whatever rejects unknown keys today keeps rejecting them; this only ever
// removes names it was told to remove.
const { allowed, blocked } = splitManagedKeys({ not_a_setting: '1' }, true);
expect(allowed).toEqual({ not_a_setting: '1' });
expect(blocked).toEqual([]);
});
});