Make registry-admin audit route self-identifying
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
import { authenticateFrontdoor, createAuthenticatedContext } from './live-frontdoor-auth.mjs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const webRoot = path.resolve(__dirname, '..');
|
||||
const outputDirectory = path.join(webRoot, 'output', 'playwright');
|
||||
const outputPath = path.join(outputDirectory, 'live-registry-admin-audit-check.json');
|
||||
const baseUrl = process.env.STELLAOPS_FRONTDOOR_BASE_URL?.trim() || 'https://stella-ops.local';
|
||||
const statePath = path.join(outputDirectory, 'live-frontdoor-auth-state.json');
|
||||
|
||||
function trimText(value, maxLength = 400) {
|
||||
const normalized = value.replace(/\s+/g, ' ').trim();
|
||||
return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
|
||||
}
|
||||
|
||||
async function collectHeadings(page) {
|
||||
return page.locator('h1, main h1, main h2, h2').evaluateAll((elements) =>
|
||||
elements
|
||||
.map((element) => (element.textContent || '').replace(/\s+/g, ' ').trim())
|
||||
.filter((text, index, values) => text.length > 0 && values.indexOf(text) === index),
|
||||
).catch(() => []);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
mkdirSync(outputDirectory, { recursive: true });
|
||||
|
||||
const authReport = await authenticateFrontdoor({
|
||||
statePath,
|
||||
reportPath: path.join(outputDirectory, 'live-frontdoor-auth-report.json'),
|
||||
});
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: ['--disable-dev-shm-usage'],
|
||||
});
|
||||
|
||||
const context = await createAuthenticatedContext(browser, authReport, { statePath });
|
||||
const page = await context.newPage();
|
||||
|
||||
const report = {
|
||||
generatedAtUtc: new Date().toISOString(),
|
||||
baseUrl,
|
||||
startUrl: `${baseUrl}/ops/integrations/registry-admin?tenant=demo-prod®ions=apac,eu-west,us-east,us-west`,
|
||||
finalUrl: '',
|
||||
headings: [],
|
||||
auditHeadingMatched: false,
|
||||
actionOk: false,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') {
|
||||
report.errors.push(`console:${message.text()}`);
|
||||
}
|
||||
});
|
||||
|
||||
page.on('pageerror', (error) => {
|
||||
report.errors.push(`page:${error.message}`);
|
||||
});
|
||||
|
||||
try {
|
||||
await page.goto(report.startUrl, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30_000,
|
||||
});
|
||||
await page.waitForTimeout(4_000);
|
||||
|
||||
const auditTab = page.locator('nav[role="tablist"] a:has-text("Audit Log")').first();
|
||||
await auditTab.click();
|
||||
await page.waitForTimeout(4_000);
|
||||
|
||||
report.finalUrl = page.url();
|
||||
report.headings = await collectHeadings(page);
|
||||
const bodyText = trimText(await page.locator('body').innerText().catch(() => ''), 1200);
|
||||
report.auditHeadingMatched = report.headings.some((heading) => /audit/i.test(heading))
|
||||
|| /registry audit log/i.test(bodyText);
|
||||
report.actionOk = report.finalUrl.includes('/registry-admin/audit')
|
||||
&& report.finalUrl.includes('tenant=')
|
||||
&& report.finalUrl.includes('regions=')
|
||||
&& report.auditHeadingMatched;
|
||||
|
||||
await page.screenshot({
|
||||
path: path.join(outputDirectory, 'live-registry-admin-audit-check.png'),
|
||||
fullPage: true,
|
||||
}).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
||||
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||
|
||||
if (!report.actionOk || report.errors.length > 0) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`[live-registry-admin-audit-check] ${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { REGISTRY_ADMIN_API } from '../../../core/api/registry-admin.client';
|
||||
import { PlanAuditComponent } from './plan-audit.component';
|
||||
|
||||
describe('PlanAuditComponent', () => {
|
||||
const registryAdminApiStub = {
|
||||
getAuditHistory: () =>
|
||||
of({
|
||||
items: [
|
||||
{
|
||||
id: 'audit-1',
|
||||
timestamp: '2026-03-11T10:15:00Z',
|
||||
action: 'Updated',
|
||||
planId: 'plan-gold',
|
||||
actor: 'admin',
|
||||
summary: 'Adjusted repository scopes',
|
||||
previousVersion: 2,
|
||||
newVersion: 3,
|
||||
},
|
||||
],
|
||||
totalCount: 1,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
}),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PlanAuditComponent],
|
||||
})
|
||||
.overrideComponent(PlanAuditComponent, {
|
||||
set: {
|
||||
providers: [{ provide: REGISTRY_ADMIN_API, useValue: registryAdminApiStub }],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
it('renders an explicit audit heading and entry count', async () => {
|
||||
const fixture = TestBed.createComponent(PlanAuditComponent);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const text = fixture.nativeElement.textContent.replace(/\s+/g, ' ').trim();
|
||||
|
||||
expect(text).toContain('Registry Audit Log');
|
||||
expect(text).toContain('Review plan mutations, actor activity, and version changes');
|
||||
expect(text).toContain('Entries');
|
||||
expect(text).toContain('Adjusted repository scopes');
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,19 @@ import { PlanAuditEntry, PaginatedResponse } from '../../../core/api/registry-ad
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="plan-audit">
|
||||
<header class="plan-audit__header">
|
||||
<div>
|
||||
<h2 class="plan-audit__title">Registry Audit Log</h2>
|
||||
<p class="plan-audit__subtitle">
|
||||
Review plan mutations, actor activity, and version changes for the registry token service.
|
||||
</p>
|
||||
</div>
|
||||
<div class="plan-audit__summary" aria-label="Audit entry count">
|
||||
<span class="plan-audit__summary-value">{{ totalCount() }}</span>
|
||||
<span class="plan-audit__summary-label">Entries</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="plan-audit__toolbar">
|
||||
<div class="plan-audit__filter">
|
||||
@@ -134,6 +147,54 @@ import { PlanAuditEntry, PaginatedResponse } from '../../../core/api/registry-ad
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.plan-audit__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.plan-audit__title {
|
||||
margin: 0 0 0.375rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.plan-audit__subtitle {
|
||||
margin: 0;
|
||||
max-width: 56rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.plan-audit__summary {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 7rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid var(--color-text-heading);
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(30, 41, 59, 0.55);
|
||||
}
|
||||
|
||||
.plan-audit__summary-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-status-info);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.plan-audit__summary-label {
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.plan-audit__toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -314,6 +375,19 @@ import { PlanAuditEntry, PaginatedResponse } from '../../../core/api/registry-ad
|
||||
border-radius: var(--radius-lg);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.plan-audit__header,
|
||||
.plan-audit__toolbar,
|
||||
.plan-audit__filter {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.plan-audit__summary {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class PlanAuditComponent implements OnInit {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"src/app/features/policy-simulation/policy-simulation-direct-route-defaults.spec.ts",
|
||||
"src/app/features/policy-simulation/policy-simulation-defaults.spec.ts",
|
||||
"src/app/features/policy-simulation/simulation-dashboard.component.spec.ts",
|
||||
"src/app/features/registry-admin/components/plan-audit.component.spec.ts",
|
||||
"src/app/features/registry-admin/registry-admin.component.spec.ts",
|
||||
"src/app/features/triage/services/ttfs-telemetry.service.spec.ts",
|
||||
"src/app/features/triage/triage-workspace.component.spec.ts",
|
||||
|
||||
Reference in New Issue
Block a user