test(260923-dhh): Proxmox-Modul Aufgabe 2 - Benutzer/Passwort, Fehlerklassen, Nur-Lesen-Riegel
- proxmox-auth.ts: loginTicket (die einzige nicht-lesende Anfrage im Modul, POST /access/ticket) und buildTicketCookieHeader je Produkt (Cookie-Namen als benannte Konstante, Annahme A2 kommentiert) - proxmox-client.service.ts: classifyFailure (401->zugang, 403->rechte, 404->antwortform, 5xx->server, Netzfehler->netz, Zertifikatsfehler-> zertifikat) und parseJsonLenient (kein Wurf bei Nicht-JSON); kein explizites method-Feld mehr an proxmoxGet (GET ist Grundwert) - proxmox.service.ts: Passwort-Zweig via Ticket-Anmeldung, genau ein zweiter Versuch nach 401 (Ticket-Ablauf alle zwei Stunden kein Fehlalarm) - proxmox-nur-lesen.spec.ts: maschinischer Riegel zu D-01 — genau eine Stelle (proxmox-auth.ts) uebergibt ein Anfrageverfahren an undiciFetch, jeder Proxmox-Pfad ausserhalb laeuft ueber proxmoxGet Tore: api 1270/1270 (>=1240), type-check 4/4. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { basename, join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
* Der maschinelle Riegel zu D-01 ("nur beobachten") — gebaut nach dem
|
||||
* Vorbild von `apps/api/src/prisma/rls-access-inventory.spec.ts`: der Test
|
||||
* liest den Quelltext, nicht das Laufzeitverhalten. Zwei Aussagen:
|
||||
*
|
||||
* 1. Die Summe der Stellen, die ein Anfrageverfahren EXPLIZIT an
|
||||
* `undiciFetch` uebergeben (`method: '...'`), ist genau
|
||||
* `EXPECTED_METHOD_PASSING_CALLS` und liegt in `proxmox-auth.ts`
|
||||
* (die Ticket-Anmeldung, `loginTicket` — die einzige nicht-lesende
|
||||
* Anfrage im gesamten Modul, D-01). `proxmoxGet` in
|
||||
* `proxmox-client.service.ts` uebergibt bewusst KEIN `method`-Feld:
|
||||
* GET ist der Grundwert von `fetch` selbst.
|
||||
* 2. Jeder gegen einen Proxmox-API-Pfad (`/api2/json/...`) gebauter Aufruf
|
||||
* ausser der Ticket-Anmeldung laeuft ueber `proxmoxGet(...)`.
|
||||
*
|
||||
* Die erwartete Zahl steht als benannte Konstante mit ausgeschriebener
|
||||
* Begruendung — eine spaetere Erhoehung erzwingt eine bewusste
|
||||
* Entscheidung, statt unbemerkt durchzurutschen (T-DHH-07).
|
||||
*/
|
||||
|
||||
/**
|
||||
* GENAU EIN Aufruf darf im gesamten Modul ein Anfrageverfahren explizit an
|
||||
* `undiciFetch` uebergeben: `loginTicket()` in `proxmox-auth.ts`
|
||||
* (`method: 'POST'`, Ticket-Anmeldung). Jede weitere Stelle waere ein neuer,
|
||||
* bislang unbedachter veraendernder Weg gegen Proxmox — T-DHH-07.
|
||||
*/
|
||||
const EXPECTED_METHOD_PASSING_CALLS = 1;
|
||||
const EXPECTED_METHOD_PASSING_FILE = 'proxmox-auth.ts';
|
||||
|
||||
const PROXMOX_SRC_DIR = join(__dirname);
|
||||
|
||||
function listTsFiles(dir: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
out.push(...listTsFiles(full));
|
||||
} else if (entry.isFile() && entry.name.endsWith('.ts')) {
|
||||
out.push(full);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Entfernt Zeilen- und Blockkommentare — Vorbild `rls-access-inventory.spec.ts`. */
|
||||
function stripComments(source: string): string {
|
||||
return source
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.split('\n')
|
||||
.filter((line) => !line.trim().startsWith('//'))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt zusaetzlich Zeichenkettenliterale (nach dem Kommentar-Entfernen)
|
||||
* — fuer Testdateien, damit eine erfundene Testkonstante wie
|
||||
* `'https://x/api2/json/...'` in einer `expect(...)`-Zeile oder ein
|
||||
* mockierter Antwortkoerper nicht als Fundstelle zaehlt (Plan-Vorgabe:
|
||||
* "entfernt vor dem Zaehlen Kommentarzeilen und Zeichenkettenliterale aus
|
||||
* Testdateien").
|
||||
*/
|
||||
function stripStringLiterals(source: string): string {
|
||||
return source
|
||||
.replace(/`(?:[^`\\]|\\.)*`/g, '``')
|
||||
.replace(/"(?:[^"\\]|\\.)*"/g, '""')
|
||||
.replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
||||
}
|
||||
|
||||
interface CallSpan {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
/** Sammelt Argumentbereiche aller Aufrufe `calleeName(...)` per Klammertiefe. */
|
||||
function collectCallArgSpans(text: string, calleeName: string): CallSpan[] {
|
||||
const spans: CallSpan[] = [];
|
||||
const re = new RegExp(`\\b${calleeName}\\(`, 'g');
|
||||
let m: RegExpExecArray | null;
|
||||
// biome-ignore lint/suspicious/noAssignInExpressions: Standard-Iterationsform der Nachbardatei rls-access-inventory.spec.ts
|
||||
while ((m = re.exec(text))) {
|
||||
const openIdx = re.lastIndex - 1;
|
||||
let depth = 0;
|
||||
let i = openIdx;
|
||||
for (; i < text.length; i++) {
|
||||
if (text[i] === '(') depth++;
|
||||
else if (text[i] === ')') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
spans.push({ start: openIdx, end: i });
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
/** Zaehlt Stellen, die `method:` innerhalb eines `undiciFetch(...)`-Aufrufs uebergeben. */
|
||||
function countMethodPassingCalls(text: string): number {
|
||||
let count = 0;
|
||||
const re = /undiciFetch\(/g;
|
||||
let m: RegExpExecArray | null;
|
||||
// biome-ignore lint/suspicious/noAssignInExpressions: s.o.
|
||||
while ((m = re.exec(text))) {
|
||||
const openIdx = re.lastIndex - 1;
|
||||
let depth = 0;
|
||||
let i = openIdx;
|
||||
for (; i < text.length; i++) {
|
||||
if (text[i] === '(') depth++;
|
||||
else if (text[i] === ')') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
const argsText = text.slice(openIdx, i + 1);
|
||||
if (/\bmethod\s*:/.test(argsText)) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Fundstellen eines Proxmox-API-Pfads ausserhalb eines `proxmoxGet(...)`-Aufrufs. */
|
||||
function findApiPathViolations(fileName: string, text: string): string[] {
|
||||
if (fileName === 'proxmox-auth.ts') {
|
||||
// Die Ticket-Anmeldung ist die eine dokumentierte Ausnahme (D-01).
|
||||
return [];
|
||||
}
|
||||
const proxmoxGetSpans = collectCallArgSpans(text, 'proxmoxGet');
|
||||
const violations: string[] = [];
|
||||
const pathRe = /\/api2\/json\/[A-Za-z0-9/{}_.-]*/g;
|
||||
let m: RegExpExecArray | null;
|
||||
// biome-ignore lint/suspicious/noAssignInExpressions: s.o.
|
||||
while ((m = pathRe.exec(text))) {
|
||||
const idx = m.index;
|
||||
const insideProxmoxGetCall = proxmoxGetSpans.some((s) => idx >= s.start && idx <= s.end);
|
||||
if (!insideProxmoxGetCall) {
|
||||
violations.push(`${fileName}@${idx}: ${m[0]}`);
|
||||
}
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
describe('proxmox-nur-lesen (D-01, T-DHH-07) — der maschinelle Riegel', () => {
|
||||
const files = listTsFiles(PROXMOX_SRC_DIR);
|
||||
|
||||
it(`genau ${EXPECTED_METHOD_PASSING_CALLS} Stelle uebergibt ein Anfrageverfahren an undiciFetch, in ${EXPECTED_METHOD_PASSING_FILE}`, () => {
|
||||
const perFile = files.map((file) => {
|
||||
const raw = readFileSync(file, 'utf-8');
|
||||
const isTest = file.endsWith('.spec.ts');
|
||||
const cleaned = isTest ? stripStringLiterals(stripComments(raw)) : stripComments(raw);
|
||||
return { file: basename(file), count: countMethodPassingCalls(cleaned) };
|
||||
});
|
||||
|
||||
const total = perFile.reduce((sum, f) => sum + f.count, 0);
|
||||
const filesWithCalls = perFile.filter((f) => f.count > 0).map((f) => f.file);
|
||||
|
||||
expect(total, `Gefundene Stellen: ${JSON.stringify(perFile.filter((f) => f.count > 0))}`).toBe(
|
||||
EXPECTED_METHOD_PASSING_CALLS,
|
||||
);
|
||||
expect(filesWithCalls).toEqual([EXPECTED_METHOD_PASSING_FILE]);
|
||||
});
|
||||
|
||||
it('jeder gegen einen Proxmox-Pfad gebaute Aufruf ausser der Ticket-Anmeldung laeuft ueber proxmoxGet', () => {
|
||||
// Nur Produktionsdateien bauen tatsaechlich Aufrufe — Testdateien
|
||||
// enthalten denselben Pfadtext nur als erwarteten Wert in `expect(...)`,
|
||||
// das ist kein "gebauter Aufruf" im Sinn dieser Aussage.
|
||||
const productionFiles = files.filter((file) => !file.endsWith('.spec.ts'));
|
||||
const violations = productionFiles.flatMap((file) => {
|
||||
const raw = readFileSync(file, 'utf-8');
|
||||
const cleaned = stripComments(raw); // Pfad-Texte bleiben erhalten — nur Kommentare raus
|
||||
return findApiPathViolations(basename(file), cleaned);
|
||||
});
|
||||
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user