mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 21:39:40 +02:00
Macht mana-analytics zur Backend-Heimat des Public-Community-Hubs. - Pseudonym: createDisplayHash(userId, secret) + generateDisplayName() produzieren deterministisch "Wachsame Eule #4528" pro User. 100 Adjektive × 80 Tieren × 10000 Suffixe → ~80M Kombinationen. 7 unit tests, 0 PII im Output. - Schema-Erweiterung user_feedback: display_hash, display_name, module_context, parent_id (1-level Threading), reactions jsonb (cached emoji→count), score (cached weighted sort). - feedback_votes ersetzt durch feedback_reactions (Slack-Pattern: unique(feedback_id, user_id, emoji), pro User mehrere Emojis möglich). - Service: createFeedback stempelt display_hash + display_name. Neue Methoden getPublicFeed (redacted), getReplies, toggleReaction (rebuilt reactions+score). Admin-Methoden adminListAll/adminUpdate founder-tier-gated im Route-Layer. - Routes: /api/v1/public/feedback/* — anonymous reads (kein Auth, kein userId/displayHash/deviceInfo im Output) /api/v1/feedback/* — auth-required Submit/React/Manage, plus :id/replies, :id/react, /admin - Config: neuer FEEDBACK_PSEUDONYM_SECRET-Env-Var seedet die Hashes; Rotation re-keyt nur künftige Pseudonyme, alte Records bleiben stabil. Migration 0002_public-community-foundation.sql idempotent, lokal + prod (mana-server) eingespielt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
49 lines
1.8 KiB
TypeScript
49 lines
1.8 KiB
TypeScript
import { describe, expect, it } from 'bun:test';
|
|
import { createDisplayHash, generateDisplayName, __TEST__ } from './pseudonym';
|
|
|
|
describe('pseudonym', () => {
|
|
it('createDisplayHash is deterministic per (userId, secret)', () => {
|
|
const a = createDisplayHash('user-123', 'secret');
|
|
const b = createDisplayHash('user-123', 'secret');
|
|
expect(a).toBe(b);
|
|
});
|
|
|
|
it('createDisplayHash varies per userId', () => {
|
|
const a = createDisplayHash('user-1', 'secret');
|
|
const b = createDisplayHash('user-2', 'secret');
|
|
expect(a).not.toBe(b);
|
|
});
|
|
|
|
it('createDisplayHash varies per secret (rotation safety)', () => {
|
|
const a = createDisplayHash('user-1', 'secret-old');
|
|
const b = createDisplayHash('user-1', 'secret-new');
|
|
expect(a).not.toBe(b);
|
|
});
|
|
|
|
it('generateDisplayName is deterministic per hash', () => {
|
|
const hash = createDisplayHash('user-123', 'secret');
|
|
expect(generateDisplayName(hash)).toBe(generateDisplayName(hash));
|
|
});
|
|
|
|
it('generateDisplayName format is "{Adj} {Tier} #{4-digit}"', () => {
|
|
const hash = createDisplayHash('user-1', 'secret');
|
|
const name = generateDisplayName(hash);
|
|
expect(name).toMatch(/^[A-ZÄÖÜ][a-zäöüß]+ [A-ZÄÖÜ][a-zäöüß]+ #\d{4}$/);
|
|
});
|
|
|
|
it('uses adjectives + animals from the curated lists', () => {
|
|
const hash = createDisplayHash('user-1', 'secret');
|
|
const [adj, animal] = generateDisplayName(hash).split(' ');
|
|
expect(__TEST__.ADJECTIVES).toContain(adj);
|
|
expect(__TEST__.ANIMALS).toContain(animal);
|
|
});
|
|
|
|
it('produces varied names across many users (no obvious clustering)', () => {
|
|
const names = new Set<string>();
|
|
for (let i = 0; i < 1000; i++) {
|
|
names.add(generateDisplayName(createDisplayHash(`user-${i}`, 'secret')));
|
|
}
|
|
// 1000 users → expect ~1000 unique pseudonyms (collisions allowed but rare).
|
|
expect(names.size).toBeGreaterThan(950);
|
|
});
|
|
});
|