feat(quick-260911-cwh): Fehlerrichtung Bereich calendar messen (Aufgabe 1)
- rls-scratch-check.mjs: elfter Abschnitt runCalendarAreaChecks mit 13 namentlich benannten Pruefungen gegen die aus 20260909140000_rls_remaining_tenant_tables geschnittene Regel, davon 4 ueber den generierten Client an einer schemagleichen Wegwerf-Tabelle (17 Spalten, gegen schema.prisma laufzeitgeprueft); Laufzeitpruefung, dass 20260910120000 keine eigene CalendarSource-Regel traegt (Messfalle 260910-jab) - Alle 101 Pruefungen bestehen (88 bisherige + 13 neue) - docs/mandantentrennung-etappe2-fehlerrichtung.md: Abschnitt "## Bereich calendar" mit (k1)-(k5) — Messung, Signaltabelle je Pfad, Leere-als-Abwesenheit in Backend UND Frontend samt Fehlerverschluckung, bewusst nicht geloest (Cache-Schluessel-Urteil, Zugangsdaten-Erhaltung, fehlende Benutzerdimension, 403/404), bewusst nicht angefasst - Wettlauf-Fehlerklasse gemessen: PrismaClientKnownRequestError (P2025), NICHT PrismaClientUnknownRequestError wie im Bereich dashboard — Aufgabe 2 braucht deshalb keine neue Fehleruebersetzung fuer die Besitzpruefungen Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -42,6 +42,7 @@ const SCRATCH_ROLE_NAME = 'tessera_rls_scratch_role';
|
||||
const SCRATCH_ROLE_PASSWORD = 'scratch_only_local_never_reused';
|
||||
const MIGRATIONS_DIR = join(__dirname, '../prisma/migrations');
|
||||
const PRISMA_BIN = join(__dirname, '../node_modules/.bin/prisma');
|
||||
const SCHEMA_PRISMA_PATH = join(__dirname, '../prisma/schema.prisma');
|
||||
|
||||
/**
|
||||
* Fuehrt ein mehrteiliges SQL-Skript (mehrere Anweisungen, DO $$ ... $$
|
||||
@@ -2765,6 +2766,336 @@ async function runDashboardAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest die Feldnamen eines Prisma-Modellblocks direkt aus
|
||||
* `apps/api/prisma/schema.prisma`, statt sie im Werkzeug zu wiederholen
|
||||
* (Pruefung 8 in `runCalendarAreaChecks`, Lehre aus Pruefung 5b im Bereich
|
||||
* `dashboard`: der generierte Client waehlt standardmaessig JEDE Spalte des
|
||||
* Modells aus und scheitert mit P2022 an jeder fehlenden — eine
|
||||
* Wegwerf-Tabelle mit unvollstaendigem Spaltensatz wuerde das nie zeigen,
|
||||
* Roh-SQL merkt es ohnehin nie). Feldname = erstes Wort jeder nicht-leeren
|
||||
* Zeile im Modellblock, die nicht mit `@@` (Modell-Attribute wie
|
||||
* `@@index`) und nicht mit `//` (Kommentarzeile) beginnt.
|
||||
*/
|
||||
function readSchemaModelFieldNames(modelName) {
|
||||
const schemaSource = readFileSync(SCHEMA_PRISMA_PATH, 'utf-8');
|
||||
const re = new RegExp(`model ${modelName} \\{([\\s\\S]*?)\\n\\}`);
|
||||
const match = schemaSource.match(re);
|
||||
if (!match) return [];
|
||||
const fields = [];
|
||||
for (const rawLine of match[1].split('\n')) {
|
||||
const line = rawLine.trim();
|
||||
if (!line) continue;
|
||||
if (line.startsWith('@@')) continue;
|
||||
if (line.startsWith('//')) continue;
|
||||
const firstWord = line.split(/\s+/)[0];
|
||||
if (firstWord) fields.push(firstWord);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aufgabe 1 (260911-cwh) — misst die zwoelf im Plan genannten
|
||||
* Verhaltensweisen des Bereichs `calendar` unter der Rolle ohne BYPASSRLS,
|
||||
* an der Regel WORTGLEICH aus der ausgelieferten Migration
|
||||
* `20260909140000_rls_remaining_tenant_tables` geschnitten — NICHT dem
|
||||
* Werkzeug nachgetippt (vgl. runDkvAreaChecks/runDashboardAreaChecks).
|
||||
*
|
||||
* Prueft zur Laufzeit zusaetzlich die Messfalle aus 260910-jab: die
|
||||
* `*_rls_widen_membership_grant_and_platform_read`-Migration (260910-jab)
|
||||
* darf KEINE eigene Regel fuer "CalendarSource" enthalten (Befund G) —
|
||||
* faende sich dort eine, waere der Regelstand nicht mehr eindeutig auf
|
||||
* 20260909140000 zurueckzufuehren und dieser Abschnitt braeche ab, statt
|
||||
* die abgeloeste Regel weiterzumessen.
|
||||
*
|
||||
* Legt die Wegwerf-Tabelle "CalendarSource" selbst neu an, mit SAEMTLICHEN
|
||||
* Spalten des Modells (nicht nur denen, die Roh-SQL braucht — Pruefung 8,
|
||||
* die dashboard-Lehre aus Pruefung 5b) und setzt auf keiner Tabelle eines
|
||||
* anderen Abschnitts auf: er ist ein Blatt in der Aufrufkette, muss NACH
|
||||
* runDashboardAreaChecks() und VOR runTransactionShapeMeasurement() laufen
|
||||
* (siehe Aufrufkette in main()) — Letztere setzt weiterhin auf der von
|
||||
* runGroupsAreaChecks() angelegten Tabelle "Group" auf, dieser Abschnitt
|
||||
* aendert daran nichts, und keine spaetere Pruefung setzt auf der hier
|
||||
* angelegten Tabelle auf.
|
||||
*/
|
||||
async function runCalendarAreaChecks(adminUrl, scratchRoleUrl, results) {
|
||||
const widenMigrationSql = readRlsWidenMigrationSql();
|
||||
const widenHasOwnCalendarSourcePolicy =
|
||||
widenMigrationSql && Boolean(extractPolicySql(widenMigrationSql, 'CalendarSource'));
|
||||
report(
|
||||
results,
|
||||
'calendarsource-regelstand-eindeutig',
|
||||
!widenHasOwnCalendarSourcePolicy,
|
||||
widenHasOwnCalendarSourcePolicy
|
||||
? 'die *_rls_widen_membership_grant_and_platform_read-Migration (260910-jab) enthaelt eine EIGENE Regel fuer "CalendarSource" — der Regelstand ist nicht mehr eindeutig auf 20260909140000_rls_remaining_tenant_tables zurueckzufuehren, Messung abgebrochen statt die abgeloeste Regel weiterzumessen'
|
||||
: 'die *_rls_widen_membership_grant_and_platform_read-Migration (260910-jab) enthaelt KEINE eigene Regel fuer "CalendarSource" (Befund G) — der Stand aus 20260909140000_rls_remaining_tenant_tables ist weiterhin der ausgelieferte, aktuelle Regelstand',
|
||||
);
|
||||
if (widenHasOwnCalendarSourcePolicy) {
|
||||
return;
|
||||
}
|
||||
|
||||
const remainingMigrationSql = readRemainingTenantTablesMigrationSql();
|
||||
const calendarSourcePolicy = remainingMigrationSql
|
||||
? extractPolicySql(remainingMigrationSql, 'CalendarSource')
|
||||
: null;
|
||||
|
||||
if (!calendarSourcePolicy) {
|
||||
report(
|
||||
results,
|
||||
'calendarsource-policy-aus-migration-gefunden',
|
||||
false,
|
||||
'CREATE POLICY fuer "CalendarSource" nicht in der ausgelieferten *_rls_remaining_tenant_tables-Migration gefunden',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await withAdminPrisma(urlForDatabase(adminUrl, SCRATCH_DB_NAME).toString(), async (db) => {
|
||||
await db.$executeRawUnsafe(`
|
||||
CREATE TABLE "CalendarSource" (
|
||||
id text PRIMARY KEY,
|
||||
"userId" text NOT NULL,
|
||||
"tenantId" text NOT NULL,
|
||||
name text NOT NULL,
|
||||
type text NOT NULL,
|
||||
"exchangeMode" text,
|
||||
domain text,
|
||||
url text NOT NULL,
|
||||
username text,
|
||||
"encryptedPassword" text,
|
||||
color text DEFAULT '#3B82F6',
|
||||
"isVisible" boolean NOT NULL DEFAULT true,
|
||||
"syncIntervalMin" integer NOT NULL DEFAULT 15,
|
||||
"lastSyncAt" timestamp(3),
|
||||
"lastSyncError" text,
|
||||
"createdAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
await db.$executeRawUnsafe(`ALTER TABLE "CalendarSource" ENABLE ROW LEVEL SECURITY;`);
|
||||
await db.$executeRawUnsafe(`ALTER TABLE "CalendarSource" FORCE ROW LEVEL SECURITY;`);
|
||||
await db.$executeRawUnsafe(calendarSourcePolicy);
|
||||
await db.$executeRawUnsafe(
|
||||
`GRANT SELECT, INSERT, UPDATE, DELETE ON "CalendarSource" TO ${SCRATCH_ROLE_NAME}`,
|
||||
);
|
||||
|
||||
// Zwei Zeilen unter TENANT-A mit VERSCHIEDENEN Benutzerkennungen
|
||||
// (Befund G — die Regel kennt keine Benutzerdimension), je mit
|
||||
// gesetztem encryptedPassword-Platzhalter, damit Pruefung 3 zeigen
|
||||
// kann, dass ein Kollege desselben Mandanten die verschluesselten
|
||||
// Zugangsdaten sieht; eine Zeile unter TENANT-B fuer die
|
||||
// Mandantengrenze.
|
||||
await db.$executeRawUnsafe(`
|
||||
INSERT INTO "CalendarSource" (id, "userId", "tenantId", name, type, url, "encryptedPassword", "isVisible") VALUES
|
||||
('source-a1', 'user-a1', 'TENANT-A', 'Quelle A1', 'ics', 'https://example.invalid/a1.ics', 'enc(a1-passwort-platzhalter)', true),
|
||||
('source-a2', 'user-a2', 'TENANT-A', 'Quelle A2', 'ics', 'https://example.invalid/a2.ics', 'enc(a2-passwort-platzhalter)', true),
|
||||
('source-b1', 'user-b1', 'TENANT-B', 'Quelle B1', 'ics', 'https://example.invalid/b1.ics', 'enc(b1-passwort-platzhalter)', true);
|
||||
`);
|
||||
});
|
||||
|
||||
// Pruefung 8 zuerst — faellt sie durch, sind die Client-Messungen (9-12)
|
||||
// wertlos, deshalb steht sie vor ihnen und die Funktion bricht ab, wenn
|
||||
// sie fehlschlaegt.
|
||||
const schemaFields = readSchemaModelFieldNames('CalendarSource');
|
||||
const tableColumns = await withAdminPrisma(
|
||||
urlForDatabase(adminUrl, SCRATCH_DB_NAME).toString(),
|
||||
async (db) => {
|
||||
const rows = await db.$queryRaw`
|
||||
SELECT column_name FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'CalendarSource'
|
||||
`;
|
||||
return rows.map((r) => r.column_name).sort();
|
||||
},
|
||||
);
|
||||
const schemaFieldsSorted = [...schemaFields].sort();
|
||||
const columnsMatch =
|
||||
schemaFieldsSorted.length > 0 &&
|
||||
schemaFieldsSorted.length === tableColumns.length &&
|
||||
schemaFieldsSorted.every((f, i) => f === tableColumns[i]);
|
||||
report(
|
||||
results,
|
||||
'calendarsource-wegwerftabelle-deckt-alle-spalten-des-generierten-clients',
|
||||
columnsMatch,
|
||||
`Schema-Felder aus schema.prisma (model CalendarSource, ${schemaFieldsSorted.length}): ${JSON.stringify(schemaFieldsSorted)}; Spalten der Wegwerf-Tabelle (${tableColumns.length}): ${JSON.stringify(tableColumns)}`,
|
||||
);
|
||||
if (!columnsMatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prisma = new PrismaClient({ datasourceUrl: scratchRoleUrl });
|
||||
try {
|
||||
// 1 + 3: calendarsource-gebunden-nur-eigener-mandant UND
|
||||
// calendarsource-fremder-nutzer-desselben-mandanten-gebunden-sichtbar —
|
||||
// eine Abfrage, zwei Aussagen. Pruefung 3 zu bestehen IST das erwartete
|
||||
// Ergebnis: die Regel kennt keine Benutzerdimension.
|
||||
const rowsForA = await forTenantQuery(prisma, 'TENANT-A', (tx) =>
|
||||
tx.$queryRaw`SELECT id, "userId", "tenantId", "encryptedPassword" FROM "CalendarSource" ORDER BY id`,
|
||||
);
|
||||
report(
|
||||
results,
|
||||
'calendarsource-gebunden-nur-eigener-mandant',
|
||||
rowsForA.length === 2 && rowsForA.every((r) => r.tenantId === 'TENANT-A'),
|
||||
`forTenant(TENANT-A) liefert ${rowsForA.length} Zeile(n): ${JSON.stringify(rowsForA.map((r) => r.id))}`,
|
||||
);
|
||||
const a2Row = rowsForA.find((r) => r.userId === 'user-a2');
|
||||
const a2CredentialsVisible = Boolean(a2Row && a2Row.encryptedPassword);
|
||||
report(
|
||||
results,
|
||||
'calendarsource-fremder-nutzer-desselben-mandanten-gebunden-sichtbar',
|
||||
a2CredentialsVisible,
|
||||
`forTenant(TENANT-A) liefert die Quelle von 'user-a2' (anderer Benutzer, gleicher Mandant) mit encryptedPassword=${JSON.stringify(a2Row?.encryptedPassword)} — die Regel auf "CalendarSource" kennt keine Benutzerdimension, die verschluesselten Zugangsdaten eines Kollegen DESSELBEN Mandanten sind auf Datenbankebene lesbar; die anwendungsseitige Filterung ueber die Benutzerkennung bleibt deshalb der einzige Schutz, bis die Etappe-3-Entscheidung (2) die Benutzerdimension nachzieht`,
|
||||
);
|
||||
|
||||
// 2: calendarsource-ungebunden-null-zeilen — die tragende Belegzeile.
|
||||
const unboundRows = await prisma.$queryRaw`SELECT "tenantId" FROM "CalendarSource"`;
|
||||
report(
|
||||
results,
|
||||
'calendarsource-ungebunden-null-zeilen',
|
||||
unboundRows.length === 0,
|
||||
`ungebundener SELECT auf "CalendarSource" liefert ${unboundRows.length} Zeile(n), tatsaechlich vorhanden sind 3`,
|
||||
);
|
||||
|
||||
// 4: calendarsource-ungebundene-einzelabfrage-ueber-kennung-liefert-keine-zeile
|
||||
// — die Datenbankseite der drei Besitzpruefungen (Befund I).
|
||||
const unboundSingleRow = await prisma.$queryRaw`SELECT id FROM "CalendarSource" WHERE id = 'source-b1'`;
|
||||
report(
|
||||
results,
|
||||
'calendarsource-ungebundene-einzelabfrage-ueber-kennung-liefert-keine-zeile',
|
||||
unboundSingleRow.length === 0,
|
||||
`ungebundenes SELECT ueber die Kennung 'source-b1' (vorhanden) liefert ${unboundSingleRow.length} Zeile(n) — die Datenbankseite der drei Besitzpruefungen: ein ungebundenes Nachschlagen ueber eine vorhandene Kennung liefert null Zeilen, das ist der Weg in NotFoundException`,
|
||||
);
|
||||
|
||||
// 5: calendarsource-gebundenes-einfuegen-fremder-mandant-abgelehnt
|
||||
let foreignInsertRejected = false;
|
||||
let foreignInsertDetail = '';
|
||||
try {
|
||||
await forTenantQuery(
|
||||
prisma,
|
||||
'TENANT-A',
|
||||
(tx) =>
|
||||
tx.$executeRaw`INSERT INTO "CalendarSource" (id, "userId", "tenantId", name, type, url) VALUES ('source-rejected', 'user-a1', 'TENANT-B', 'Sollte abgewiesen werden', 'ics', 'https://example.invalid/rejected.ics')`,
|
||||
);
|
||||
foreignInsertDetail = 'gebundenes INSERT unter TENANT-A mit tenantId=TENANT-B ist NICHT fehlgeschlagen';
|
||||
} catch (err) {
|
||||
const sqlState = sqlStateOf(err);
|
||||
foreignInsertRejected = sqlState === '42501';
|
||||
foreignInsertDetail = `gebundenes INSERT unter TENANT-A mit tenantId=TENANT-B abgewiesen mit SQLSTATE ${sqlState ?? 'unbekannt'} (${err.message.trim()})`;
|
||||
}
|
||||
report(
|
||||
results,
|
||||
'calendarsource-gebundenes-einfuegen-fremder-mandant-abgelehnt',
|
||||
foreignInsertRejected,
|
||||
foreignInsertDetail,
|
||||
);
|
||||
|
||||
// 6: calendarsource-gebundenes-loeschen-fremder-zeile-trifft-keine-zeile
|
||||
const foreignDeleteAffected = await forTenantQuery(
|
||||
prisma,
|
||||
'TENANT-A',
|
||||
(tx) => tx.$executeRaw`DELETE FROM "CalendarSource" WHERE id = 'source-b1'`,
|
||||
);
|
||||
const stillThereAfterDelete = await withAdminPrisma(
|
||||
urlForDatabase(adminUrl, SCRATCH_DB_NAME).toString(),
|
||||
async (db) => {
|
||||
const rows = await db.$queryRaw`SELECT id FROM "CalendarSource" WHERE id = 'source-b1'`;
|
||||
return rows.length === 1;
|
||||
},
|
||||
);
|
||||
report(
|
||||
results,
|
||||
'calendarsource-gebundenes-loeschen-fremder-zeile-trifft-keine-zeile',
|
||||
foreignDeleteAffected === 0 && stillThereAfterDelete,
|
||||
`gebundenes DELETE unter TENANT-A ueber die Kennung 'source-b1' (gehoert TENANT-B) trifft ${foreignDeleteAffected} Zeile(n); ueber die Wartungsrolle ist die Zeile danach noch vorhanden: ${stillThereAfterDelete}`,
|
||||
);
|
||||
|
||||
// 7: calendarsource-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen
|
||||
const foreignUpdateAffected = await forTenantQuery(
|
||||
prisma,
|
||||
'TENANT-A',
|
||||
(tx) => tx.$executeRaw`UPDATE "CalendarSource" SET "lastSyncError" = 'x' WHERE id = 'source-b1'`,
|
||||
);
|
||||
report(
|
||||
results,
|
||||
'calendarsource-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen',
|
||||
foreignUpdateAffected === 0,
|
||||
`gebundenes UPDATE ... WHERE id = 'source-b1' (gehoert TENANT-B) unter TENANT-A trifft ${foreignUpdateAffected} Zeile(n), lastSyncError bleibt unveraendert`,
|
||||
);
|
||||
|
||||
// 9: calendarsource-generierter-client-gebundene-quellenliste-nur-eigener-mandant
|
||||
const bound = buildInlineExtendedClient(prisma, 'TENANT-A');
|
||||
const clientBoundSources = await bound.calendarSource.findMany({
|
||||
where: { userId: 'user-a1', isVisible: true },
|
||||
});
|
||||
report(
|
||||
results,
|
||||
'calendarsource-generierter-client-gebundene-quellenliste-nur-eigener-mandant',
|
||||
clientBoundSources.length === 1 && clientBoundSources[0].id === 'source-a1',
|
||||
`bound.calendarSource.findMany({ where: { userId: 'user-a1', isVisible: true } }) unter TENANT-A liefert ${clientBoundSources.length} Zeile(n): ${JSON.stringify(clientBoundSources.map((s) => s.id))} — die Abfrage, die fetchAndCacheEvents stellt`,
|
||||
);
|
||||
|
||||
// 10: calendarsource-generierter-client-ungebundene-quellenliste-null-zeilen
|
||||
const clientUnboundSources = await prisma.calendarSource.findMany({
|
||||
where: { userId: 'user-a1', isVisible: true },
|
||||
});
|
||||
report(
|
||||
results,
|
||||
'calendarsource-generierter-client-ungebundene-quellenliste-null-zeilen',
|
||||
clientUnboundSources.length === 0,
|
||||
`dieselbe Abfrage auf dem UNGEBUNDENEN generierten Client liefert ${clientUnboundSources.length} Zeile(n) ohne Fehler — exakt der Wert, den getSources als "keine Quelle" und fetchAndCacheEvents als "keine Termine" weiterreicht`,
|
||||
);
|
||||
|
||||
// 11: calendarsource-generierter-client-gebundenes-update-auf-unsichtbare-zeile-scheitert-laut
|
||||
let updateOnInvisibleThrew = false;
|
||||
let updateOnInvisibleDetail = '';
|
||||
try {
|
||||
await bound.calendarSource.update({
|
||||
where: { id: 'source-b1' },
|
||||
data: { lastSyncError: 'x' },
|
||||
});
|
||||
updateOnInvisibleDetail =
|
||||
'bound.calendarSource.update unter TENANT-A auf die unter TENANT-B unsichtbare Zeile (source-b1) ist NICHT fehlgeschlagen';
|
||||
} catch (err) {
|
||||
updateOnInvisibleThrew = true;
|
||||
const ctor = err?.constructor?.name ?? 'unbekannt';
|
||||
updateOnInvisibleDetail = `bound.calendarSource.update unter TENANT-A auf die unter TENANT-B unsichtbare Zeile (source-b1) wirft ${ctor}${err?.code ? ` (code ${err.code})` : ''}: ${(err.message ?? '').toString().trim()}`;
|
||||
}
|
||||
report(
|
||||
results,
|
||||
'calendarsource-generierter-client-gebundenes-update-auf-unsichtbare-zeile-scheitert-laut',
|
||||
updateOnInvisibleThrew,
|
||||
updateOnInvisibleDetail,
|
||||
);
|
||||
|
||||
// 12: calendarsource-generierter-client-gebundenes-anlegen-eigener-mandant-gelingt
|
||||
let createSucceeded = false;
|
||||
let createDetail = '';
|
||||
try {
|
||||
const created = await bound.calendarSource.create({
|
||||
data: {
|
||||
userId: 'user-a1',
|
||||
tenantId: 'TENANT-A',
|
||||
name: 'Neu angelegte Quelle',
|
||||
type: 'ics',
|
||||
url: 'https://example.invalid/neu.ics',
|
||||
},
|
||||
});
|
||||
const readBack = await bound.calendarSource.findMany({ where: { id: created.id } });
|
||||
createSucceeded = readBack.length === 1;
|
||||
createDetail = `bound.calendarSource.create unter TENANT-A gelingt (id=${created.id}, createdAt=${JSON.stringify(created.createdAt)}), gebunden lesbar: ${createSucceeded} — bestaetigt nebenbei, dass die Wegwerf-Tabelle die clientseitig erzeugten Werte (id, createdAt, updatedAt) annimmt`;
|
||||
} catch (err) {
|
||||
createDetail = `bound.calendarSource.create unter TENANT-A ist fehlgeschlagen: ${err.message}`;
|
||||
}
|
||||
report(
|
||||
results,
|
||||
'calendarsource-generierter-client-gebundenes-anlegen-eigener-mandant-gelingt',
|
||||
createSucceeded,
|
||||
createDetail,
|
||||
);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aufgabe 1 (260909-jts), TEIL 2 — misst, welche der drei Transaktionsformen
|
||||
* den Mandantenkontext auf DERSELBEN Verbindung ueber alle Teilschritte
|
||||
@@ -3001,6 +3332,7 @@ async function main() {
|
||||
await runUserAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||
await runModuleRegistryAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||
await runDashboardAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||
await runCalendarAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||
await runTransactionShapeMeasurement(scratchRoleUrlString, results);
|
||||
await runConcurrencyProbe(scratchRoleUrlString, results);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user