feat: add privacy-aware security manager onboarding
All checks were successful
Codex Template Compliance / template-compliance (pull_request) Successful in 7s
Build / test-and-image (pull_request) Successful in 52s

This commit is contained in:
2026-07-05 21:39:55 +02:00
parent d13652a70b
commit 0c7ddc53b4
14 changed files with 767 additions and 8 deletions

View File

@@ -0,0 +1,74 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { SecurityProfileStore } from '../lib/security/security-profile-store.mjs';
import { SecurityOnboarding } from '../lib/security/security-onboarding.mjs';
function setup(secret = 'test-only-security-profile-key-at-least-32-chars') {
const sent = [];
const alerter = {
isConfigured: true,
async sendMessage(text, options) {
sent.push({ text, options });
return { ok: true };
},
};
const path = join(mkdtempSync(join(tmpdir(), 'security-onboarding-')), 'profile.enc');
const store = new SecurityProfileStore(path, secret).init();
return { sent, store, onboarding: new SecurityOnboarding({ store, alerter, chatId: '42' }) };
}
function query() {
return { message: { chat: { id: 42 } } };
}
test('first startup asks only for language before personal information', async () => {
const { sent, onboarding } = setup();
assert.equal(await onboarding.ensureStarted(), true);
assert.equal(sent.length, 1);
assert.match(sent[0].text, /language|Sprache/i);
assert.deepEqual(sent[0].options.replyMarkup.inline_keyboard[0].map(button => button.callback_data), [
'security_language:de',
'security_language:en',
]);
assert.doesNotMatch(sent[0].text, /city|country|address|Stadt|Land|Adresse/i);
});
test('minimal onboarding saves only after review confirmation', async () => {
const { store, onboarding } = setup();
await onboarding.start(42);
let result = await onboarding.handleCallback('security_language:de', query());
assert.equal(result.handled, true);
assert.match(result.response.text, /verschl/iu);
result = await onboarding.handleCallback('security_consent:minimal', query());
assert.match(result.response.text, /Land/);
const answers = ['Deutschland', 'NRW', 'Koeln', 'Europe/Berlin', 'weather,cyber', 'important', '22:00-07:00'];
for (const answer of answers) result = onboarding.handleMessage(answer, query().message);
assert.equal(store.exists, false);
assert.match(result.response.text, /pr.f/iu);
result = await onboarding.handleCallback('security_review:save', query());
assert.equal(store.exists, true);
assert.equal(store.getProfile().language, 'de');
assert.equal(store.getProfile().location.city, 'Koeln');
assert.equal(result.handled, true);
});
test('profile deletion requires explicit callback confirmation', async () => {
const { store, onboarding } = setup();
store.save({ language: 'en', location: { country: 'DE' }, consentedAt: new Date().toISOString() });
assert.match(onboarding.deletePrompt().text, /Delete/);
await onboarding.handleCallback('security_delete:cancel', query());
assert.equal(store.exists, true);
await onboarding.handleCallback('security_delete:confirm', query());
assert.equal(store.exists, false);
});
test('setup remains unavailable without a valid encryption key', async () => {
const { sent, onboarding } = setup('short');
assert.equal(await onboarding.ensureStarted(), false);
assert.match(sent[0].text, /SECURITY_PROFILE_ENCRYPTION_KEY/);
});