feat(quick-260914-eym): Mail-Transport je Versand nach Mandant (WINDOWS #30), ldap/digest/matching ueber Systemkontext, vier Tabellen im Werkzeug, Erlaubnisliste vollstaendig
- mail: MailerModule-Fabrik und DB-Startpfad (findFirst beim Boot) ersatzlos entfernt; MailService baut je Versand einen nodemailer-Transport aus getDecryptedSmtpConfig(tenantId) des Empfaenger-Mandanten, Umgebungs-Kette (MAIL_* -> TESSERA_SMTP_* -> localhost:1025) nur als Rueckfall; Fehler weiter verschluckt (T-02-12), close() im finally; neue mail.service.spec.ts (4 Tests, T-GWH-03 geschlossen) - settings: Startpfad-Methode samt vier Spec-Tests geloescht; auth: requestPasswordReset reicht user.tenantId durch (Spec-Zusicherung) - ldap: getAllActiveConfigs und Nachverschluesselung lesen ueber forSystem (zwei Zuweisungen), Schreibzeile je Altzeile ueber forTenant(config.tenantId); Tests 301/306 umgedreht, neuer Altzeilen-Test - tender-digest: Kandidatenabfrage ueber forSystem, Schleife gebunden (+1 Test) - tender-matching: Profilabfrage ueber forSystem, Katalog (D-03) ungebunden (+1 Test) - tender-notifications.integration.spec: Mock um forSystem - Werkzeug: LdapConfig (15 Spalten), LdapFieldMapping (6), TenderMatch (8), TenderSavedSearch (8) je neun Kennungen plus Relations-Kennung ldapconfig-systemkontext-include-fieldmappings-beider-mandanten -> Alle 253 Pruefungen bestanden - Detektor: FORSYSTEM_ALLOWED_CALL_SITES auf 4 Dateien / 5 Aufrufe; Proben-Empfaenger sysPrisma (Gate-Zaehlung, Name nicht hartkodiert) - Klassifikation: 6 Zeilen system-gebunden, settings/smtpConfig gebunden - Falsifizierung durch Rueckbau ausgefuehrt und zurueckgenommen: (a) FOR SELECT bei TenderMatch entfernt -> 5 von 253 rot (Insert gelingt, cmd ALL); (b) Regel TenderSavedSearch aus der Datei entfernt -> 1 von 245 rot (Extraktion), lebende DB bleibt bei 34; (c) local=false -> gruen, plus Reset entfernt -> 5 rot (Erben sichtbar); (d) Zahl 0 -> 2 rot, Fremddatei admin-seed -> 3 rot - Baseline: 64 Dateien / 1054 Tests, tsc 0, Werkzeug 253 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
@@ -5549,11 +5549,25 @@ async function runSystemContextChecks(adminUrl, scratchRoleUrl, results) {
|
||||
report(results, 'system-context-migration-gefunden', false, 'Migration "_rls_system_context_read" nicht gefunden');
|
||||
return;
|
||||
}
|
||||
// Mandantenregeln je Tabelle aus IHRER Quellmigration (Aufgabe 2, 260914-eym):
|
||||
// DkvModuleConfig/TenderMatch aus _rls_remaining_tenant_tables, LdapConfig/
|
||||
// LdapFieldMapping aus _rls_policies, TenderSavedSearch aus
|
||||
// _rls_user_dimension_personal_tables.
|
||||
const remainingTablesMigrationSql = readRemainingTenantTablesMigrationSql();
|
||||
if (!remainingTablesMigrationSql) {
|
||||
report(results, 'system-context-remaining-tables-migration-gefunden', false, 'Migration "_rls_remaining_tenant_tables" nicht gefunden');
|
||||
return;
|
||||
}
|
||||
const rlsPoliciesMigrationSql = readRlsPoliciesMigrationSql();
|
||||
if (!rlsPoliciesMigrationSql) {
|
||||
report(results, 'system-context-rls-policies-migration-gefunden', false, 'Migration "_rls_policies" nicht gefunden');
|
||||
return;
|
||||
}
|
||||
const userDimensionMigrationSql = readRlsUserDimensionMigrationSql();
|
||||
if (!userDimensionMigrationSql) {
|
||||
report(results, 'system-context-user-dimension-migration-gefunden', false, 'Migration "_rls_user_dimension_personal_tables" nicht gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Vier Funktionsfaelle, je in einer eigenen Transaktion.
|
||||
const prisma = new PrismaClient({ datasourceUrl: scratchRoleUrl });
|
||||
@@ -5623,6 +5637,198 @@ async function runSystemContextChecks(adminUrl, scratchRoleUrl, results) {
|
||||
createAttemptData: { id: 'cfg-system-schreibversuch', tenantId: 'TENANT-A', isActive: true },
|
||||
updateManyData: { folder: 'SYSTEM-SCHREIBVERSUCH' },
|
||||
});
|
||||
|
||||
// Aufgabe 2 (260914-eym): die vier weiteren Tabellen ueber dieselbe Routine.
|
||||
|
||||
// LdapConfig — Mandantenregel aus 20260618112133_rls_policies, alle 15
|
||||
// skalaren Spalten (text[]-Spalten mit DEFAULT '{}'). MUSS vor
|
||||
// LdapFieldMapping laufen (deren Regel joint auf "LdapConfig").
|
||||
await runSystemContextTableCheck({
|
||||
adminUrl,
|
||||
scratchRoleUrl,
|
||||
results,
|
||||
slug: 'ldapconfig',
|
||||
tableName: 'LdapConfig',
|
||||
modelName: 'LdapConfig',
|
||||
tenantPolicySql: extractPolicySql(rlsPoliciesMigrationSql, 'LdapConfig'),
|
||||
systemContextMigrationSql,
|
||||
createTableSql: `
|
||||
CREATE TABLE "LdapConfig" (
|
||||
id text PRIMARY KEY,
|
||||
"tenantId" text NOT NULL UNIQUE,
|
||||
"serverUrl" text NOT NULL,
|
||||
"baseDn" text NOT NULL,
|
||||
"bindDn" text,
|
||||
"encryptedBindPassword" text,
|
||||
"searchFilter" text NOT NULL DEFAULT '(objectClass=person)',
|
||||
"syncIntervalMin" integer NOT NULL DEFAULT 0,
|
||||
"isActive" boolean NOT NULL DEFAULT true,
|
||||
"tlsRejectUnauthorized" boolean NOT NULL DEFAULT true,
|
||||
"groupFilterDns" text[] NOT NULL DEFAULT '{}',
|
||||
"userExcludeList" text[] NOT NULL DEFAULT '{}',
|
||||
"lastSyncAt" timestamp(3),
|
||||
"createdAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`,
|
||||
seedSql: `
|
||||
INSERT INTO "LdapConfig" (id, "tenantId", "serverUrl", "baseDn", "isActive") VALUES
|
||||
('cfg-a', 'TENANT-A', 'ldap://a.example.invalid', 'dc=a', true),
|
||||
('cfg-b', 'TENANT-B', 'ldap://b.example.invalid', 'dc=b', true);
|
||||
`,
|
||||
tenantOfRow: (row) => row.tenantId,
|
||||
createAttemptData: {
|
||||
id: 'cfg-system-schreibversuch',
|
||||
tenantId: 'TENANT-A',
|
||||
serverUrl: 'ldap://x.example.invalid',
|
||||
baseDn: 'dc=x',
|
||||
},
|
||||
updateManyData: { searchFilter: '(cn=SYSTEM-SCHREIBVERSUCH)' },
|
||||
});
|
||||
|
||||
// LdapFieldMapping — Regel aus derselben Datei (Join auf LdapConfig), 6
|
||||
// Spalten, ohne DROP der Elternzeilen (cfg-a/cfg-b bleiben stehen); der
|
||||
// Mandant einer Zeile ergibt sich ueber ldapConfigId.
|
||||
const tenantOfMapping = (row) => (row.ldapConfigId === 'cfg-a' ? 'TENANT-A' : row.ldapConfigId === 'cfg-b' ? 'TENANT-B' : row.ldapConfigId);
|
||||
await runSystemContextTableCheck({
|
||||
adminUrl,
|
||||
scratchRoleUrl,
|
||||
results,
|
||||
slug: 'ldapfieldmapping',
|
||||
tableName: 'LdapFieldMapping',
|
||||
modelName: 'LdapFieldMapping',
|
||||
tenantPolicySql: extractPolicySql(rlsPoliciesMigrationSql, 'LdapFieldMapping'),
|
||||
systemContextMigrationSql,
|
||||
createTableSql: `
|
||||
CREATE TABLE "LdapFieldMapping" (
|
||||
id text PRIMARY KEY,
|
||||
"ldapConfigId" text NOT NULL REFERENCES "LdapConfig"(id) ON DELETE CASCADE,
|
||||
"ldapField" text NOT NULL,
|
||||
"tesseraField" text NOT NULL,
|
||||
"isDefault" boolean NOT NULL DEFAULT false,
|
||||
"createdAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE ("ldapConfigId", "ldapField")
|
||||
);
|
||||
`,
|
||||
seedSql: `
|
||||
INSERT INTO "LdapFieldMapping" (id, "ldapConfigId", "ldapField", "tesseraField") VALUES
|
||||
('fm-a', 'cfg-a', 'mail', 'email'),
|
||||
('fm-b', 'cfg-b', 'mail', 'email');
|
||||
`,
|
||||
tenantOfRow: tenantOfMapping,
|
||||
createAttemptData: {
|
||||
id: 'fm-system-schreibversuch',
|
||||
ldapConfigId: 'cfg-a',
|
||||
ldapField: 'sn',
|
||||
tesseraField: 'lastName',
|
||||
},
|
||||
updateManyData: { tesseraField: 'SYSTEM-SCHREIBVERSUCH' },
|
||||
});
|
||||
|
||||
// Relations-Kennung: die #27-Form unter Systemkontext — ldapConfig.findMany
|
||||
// mit include: { fieldMappings } liefert beide Mandanten und je genau eine
|
||||
// Zuordnung (der Pfad von LdapConfigService.getAllActiveConfigs()).
|
||||
{
|
||||
const prisma = new PrismaClient({ datasourceUrl: scratchRoleUrl });
|
||||
try {
|
||||
const rows = await buildInlineSystemClient(prisma).ldapConfig.findMany({
|
||||
where: { isActive: true },
|
||||
include: { fieldMappings: true },
|
||||
orderBy: { tenantId: 'asc' },
|
||||
});
|
||||
const shape = rows.map((r) => `${r.tenantId}:${r.fieldMappings.length}`);
|
||||
report(
|
||||
results,
|
||||
'ldapconfig-systemkontext-include-fieldmappings-beider-mandanten',
|
||||
rows.length === 2 && shape.join(',') === 'TENANT-A:1,TENANT-B:1',
|
||||
`system.ldapConfig.findMany({ where: { isActive: true }, include: { fieldMappings: true } }) liefert ${rows.length} Zeile(n): ${JSON.stringify(shape)} (Mandant:Anzahl Zuordnungen)`,
|
||||
);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// TenderMatch — Regel aus 20260909140000_rls_remaining_tenant_tables, 8
|
||||
// Spalten, je Mandant eine Zeile mit notifiedAt NULL (die Kandidatenform
|
||||
// des Digest). Keine Fremdschluessel in der Wegwerf-Tabelle — gemessen
|
||||
// wird die Regel, nicht die Referenz.
|
||||
await runSystemContextTableCheck({
|
||||
adminUrl,
|
||||
scratchRoleUrl,
|
||||
results,
|
||||
slug: 'tendermatch',
|
||||
tableName: 'TenderMatch',
|
||||
modelName: 'TenderMatch',
|
||||
tenantPolicySql: extractPolicySql(remainingTablesMigrationSql, 'TenderMatch'),
|
||||
systemContextMigrationSql,
|
||||
createTableSql: `
|
||||
CREATE TABLE "TenderMatch" (
|
||||
id text PRIMARY KEY,
|
||||
"tenderId" text NOT NULL,
|
||||
"savedSearchId" text NOT NULL,
|
||||
"userId" text NOT NULL,
|
||||
"tenantId" text NOT NULL,
|
||||
"matchedAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"notifiedAt" timestamp(3),
|
||||
"notifiedChannel" text,
|
||||
UNIQUE ("tenderId", "savedSearchId")
|
||||
);
|
||||
`,
|
||||
seedSql: `
|
||||
INSERT INTO "TenderMatch" (id, "tenderId", "savedSearchId", "userId", "tenantId", "notifiedAt") VALUES
|
||||
('tm-a', 'tender-1', 'ss-a', 'user-a', 'TENANT-A', NULL),
|
||||
('tm-b', 'tender-1', 'ss-b', 'user-b', 'TENANT-B', NULL);
|
||||
`,
|
||||
tenantOfRow: (row) => row.tenantId,
|
||||
createAttemptData: {
|
||||
id: 'tm-system-schreibversuch',
|
||||
tenderId: 'tender-2',
|
||||
savedSearchId: 'ss-a',
|
||||
userId: 'user-a',
|
||||
tenantId: 'TENANT-A',
|
||||
},
|
||||
updateManyData: { notifiedChannel: 'SYSTEM-SCHREIBVERSUCH' },
|
||||
});
|
||||
|
||||
// TenderSavedSearch — Regel aus 20260911120000_rls_user_dimension_personal_tables
|
||||
// (IS-NULL-OR-Form), 8 Spalten.
|
||||
await runSystemContextTableCheck({
|
||||
adminUrl,
|
||||
scratchRoleUrl,
|
||||
results,
|
||||
slug: 'tendersavedsearch',
|
||||
tableName: 'TenderSavedSearch',
|
||||
modelName: 'TenderSavedSearch',
|
||||
tenantPolicySql: extractPolicySql(userDimensionMigrationSql, 'TenderSavedSearch'),
|
||||
systemContextMigrationSql,
|
||||
createTableSql: `
|
||||
CREATE TABLE "TenderSavedSearch" (
|
||||
id text PRIMARY KEY,
|
||||
"userId" text NOT NULL,
|
||||
"tenantId" text NOT NULL,
|
||||
name text NOT NULL,
|
||||
filters jsonb NOT NULL DEFAULT '{}',
|
||||
"instantAlert" boolean NOT NULL DEFAULT false,
|
||||
"createdAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE ("userId", name)
|
||||
);
|
||||
`,
|
||||
seedSql: `
|
||||
INSERT INTO "TenderSavedSearch" (id, "userId", "tenantId", name, filters) VALUES
|
||||
('ss-a', 'user-a', 'TENANT-A', 'Profil A', '{}'),
|
||||
('ss-b', 'user-b', 'TENANT-B', 'Profil B', '{}');
|
||||
`,
|
||||
tenantOfRow: (row) => row.tenantId,
|
||||
createAttemptData: {
|
||||
id: 'ss-system-schreibversuch',
|
||||
userId: 'user-a',
|
||||
tenantId: 'TENANT-A',
|
||||
name: 'Schreibversuch',
|
||||
filters: {},
|
||||
},
|
||||
updateManyData: { name: 'SYSTEM-SCHREIBVERSUCH' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user