Add MongoDB storage library and update acceptance tests with deterministic stubs
- Created StellaOps.Notify.Storage.Mongo project with initial configuration. - Added expected output files for acceptance tests (at1.txt to at10.txt). - Added fixture input files for acceptance tests (at1 to at10). - Created input and signature files for test cases fc1 to fc5.
This commit is contained in:
@@ -86,6 +86,26 @@ describe('PolicyApprovalsComponent', () => {
|
||||
expect(reviews[1].reviewerId).toBe('user-b');
|
||||
});
|
||||
|
||||
it('includes schedule fields in submission payload', () => {
|
||||
component.submitForm.patchValue({
|
||||
message: 'Please review',
|
||||
scheduleStart: '2025-12-10T00:00',
|
||||
scheduleEnd: '2025-12-11T00:00',
|
||||
});
|
||||
|
||||
component.onSubmit();
|
||||
|
||||
expect(api.submitForReview).toHaveBeenCalledWith({
|
||||
policyId: 'pack-1',
|
||||
version: '1.0.0',
|
||||
message: 'Please review',
|
||||
coverageResults: undefined,
|
||||
simulationDiff: undefined,
|
||||
scheduleStart: '2025-12-10T00:00',
|
||||
scheduleEnd: '2025-12-11T00:00',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls addReview with decision', fakeAsync(() => {
|
||||
component.reviewForm.setValue({ comment: 'Approve now' });
|
||||
component.onReview('approve');
|
||||
|
||||
@@ -59,6 +59,16 @@ import { PolicyApiService } from '../services/policy-api.service';
|
||||
<span>Simulation diff reference (optional)</span>
|
||||
<input formControlName="simulationDiff" placeholder="Run ID or artifact path" />
|
||||
</label>
|
||||
<div class="grid">
|
||||
<label class="field">
|
||||
<span>Scope start (UTC)</span>
|
||||
<input type="datetime-local" formControlName="scheduleStart" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Scope end (UTC)</span>
|
||||
<input type="datetime-local" formControlName="scheduleEnd" />
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn" type="submit" [disabled]="submitForm.invalid || submitting">{{ submitting ? 'Submitting…' : 'Submit for review' }}</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -275,6 +285,8 @@ export class PolicyApprovalsComponent {
|
||||
message: ['', [Validators.required, Validators.minLength(5)]],
|
||||
coverageResults: [''],
|
||||
simulationDiff: [''],
|
||||
scheduleStart: [''],
|
||||
scheduleEnd: [''],
|
||||
});
|
||||
|
||||
protected readonly reviewForm = this.fb.group({
|
||||
@@ -311,6 +323,8 @@ export class PolicyApprovalsComponent {
|
||||
message: this.submitForm.value.message ?? '',
|
||||
coverageResults: this.submitForm.value.coverageResults ?? undefined,
|
||||
simulationDiff: this.submitForm.value.simulationDiff ?? undefined,
|
||||
scheduleStart: this.submitForm.value.scheduleStart ?? undefined,
|
||||
scheduleEnd: this.submitForm.value.scheduleEnd ?? undefined,
|
||||
};
|
||||
|
||||
this.submitting = true;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { PolicyApiService } from '../services/policy-api.service';
|
||||
import { SimulationResult } from '../models/policy.models';
|
||||
import jsPDF from 'jspdf';
|
||||
|
||||
@Component({
|
||||
selector: 'app-policy-explain',
|
||||
@@ -20,7 +21,7 @@ import { SimulationResult } from '../models/policy.models';
|
||||
</div>
|
||||
<div class="expl__meta">
|
||||
<button type="button" (click)="exportJson()">Export JSON</button>
|
||||
<button type="button" disabled title="PDF export pending backend">Export PDF</button>
|
||||
<button type="button" (click)="exportPdf()">Export PDF</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -115,4 +116,19 @@ export class PolicyExplainComponent {
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
protected exportPdf(): void {
|
||||
if (!this.result) return;
|
||||
const doc = new jsPDF();
|
||||
doc.setFontSize(12);
|
||||
doc.text(`Run: ${this.result.runId}`, 10, 15);
|
||||
doc.text(`Policy: ${this.result.policyId} v${this.result.policyVersion}`, 10, 22);
|
||||
doc.text(`Findings: ${this.result.findings.length}`, 10, 29);
|
||||
const trace = (this.result.explainTrace ?? []).slice(0, 5);
|
||||
doc.text('Explain (first 5 steps):', 10, 38);
|
||||
trace.forEach((t, idx) => {
|
||||
doc.text(`- ${t.step}: ${t.ruleName} matched=${t.matched}`, 12, 46 + idx * 7);
|
||||
});
|
||||
doc.save(`policy-explain-${this.result.runId}.pdf`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export class PolicyPackStore {
|
||||
private readonly api = inject(PolicyApiService);
|
||||
private readonly packs$ = new BehaviorSubject<PolicyPackSummary[] | null>(null);
|
||||
private loading = false;
|
||||
private readonly cacheKey = 'policy-studio:packs-cache';
|
||||
|
||||
getPacks(): Observable<PolicyPackSummary[]> {
|
||||
if (!this.packs$.value && !this.loading) {
|
||||
@@ -25,13 +26,23 @@ export class PolicyPackStore {
|
||||
|
||||
private fetch(): void {
|
||||
this.loading = true;
|
||||
const cached = this.readCache();
|
||||
if (cached) {
|
||||
this.packs$.next(cached);
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.api
|
||||
.listPacks({ limit: 50 })
|
||||
.pipe(
|
||||
catchError(() => of(this.fallbackPacks())),
|
||||
finalize(() => (this.loading = false))
|
||||
)
|
||||
.subscribe((packs) => this.packs$.next(packs));
|
||||
.subscribe((packs) => {
|
||||
this.packs$.next(packs);
|
||||
this.writeCache(packs);
|
||||
});
|
||||
}
|
||||
|
||||
private fallbackPacks(): PolicyPackSummary[] {
|
||||
@@ -50,4 +61,22 @@ export class PolicyPackStore {
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private readCache(): PolicyPackSummary[] | null {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(this.cacheKey);
|
||||
if (!raw) return null;
|
||||
return JSON.parse(raw) as PolicyPackSummary[];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private writeCache(packs: PolicyPackSummary[]): void {
|
||||
try {
|
||||
sessionStorage.setItem(this.cacheKey, JSON.stringify(packs));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ import { PolicyPackStore } from '../services/policy-pack.store';
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="workspace__banner" *ngIf="scopeHint">
|
||||
{{ scopeHint }} — some actions are disabled. Request scopes from your admin.
|
||||
</div>
|
||||
|
||||
<div class="workspace__grid">
|
||||
<article class="pack-card" *ngFor="let pack of packs">
|
||||
<header class="pack-card__head">
|
||||
@@ -90,6 +94,9 @@ import { PolicyPackStore } from '../services/policy-pack.store';
|
||||
</dl>
|
||||
</article>
|
||||
</div>
|
||||
<div class="workspace__footer">
|
||||
<button type="button" (click)="refresh()" [disabled]="refreshing">{{ refreshing ? 'Refreshing…' : 'Refresh packs' }}</button>
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
styles: [
|
||||
@@ -110,9 +117,13 @@ import { PolicyPackStore } from '../services/policy-pack.store';
|
||||
.pack-card__actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||||
.pack-card__actions a { color: #e5e7eb; border: 1px solid #334155; border-radius: 8px; padding: 0.35rem 0.6rem; text-decoration: none; }
|
||||
.pack-card__actions a:hover { border-color: #22d3ee; }
|
||||
.pack-card__actions a.action-disabled { opacity: 0.5; pointer-events: none; border-style: dashed; }
|
||||
.pack-card__detail { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.35rem 1rem; margin: 0; }
|
||||
dt { color: #94a3b8; font-size: 0.85rem; margin: 0; }
|
||||
dd { margin: 0; color: #e5e7eb; }
|
||||
.workspace__banner { background: #1f2937; border: 1px solid #334155; color: #fbbf24; padding: 0.75rem 1rem; border-radius: 10px; margin: 0.5rem 0 1rem; }
|
||||
.workspace__footer { margin-top: 0.8rem; }
|
||||
.workspace__footer button { background: #2563eb; border: 1px solid #2563eb; color: #e5e7eb; border-radius: 8px; padding: 0.45rem 0.8rem; }
|
||||
`,
|
||||
],
|
||||
})
|
||||
@@ -124,6 +135,7 @@ export class PolicyWorkspaceComponent {
|
||||
protected canReview = false;
|
||||
protected canView = false;
|
||||
protected scopeHint = '';
|
||||
protected refreshing = false;
|
||||
|
||||
private readonly packStore = inject(PolicyPackStore);
|
||||
private readonly auth = inject(AUTH_SERVICE) as AuthService;
|
||||
@@ -139,6 +151,17 @@ export class PolicyWorkspaceComponent {
|
||||
});
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.refreshing = true;
|
||||
this.packStore.refresh();
|
||||
this.packStore.getPacks().subscribe((packs) => {
|
||||
this.packs = [...packs].sort((a, b) =>
|
||||
b.modifiedAt.localeCompare(a.modifiedAt) || a.id.localeCompare(b.id)
|
||||
);
|
||||
this.refreshing = false;
|
||||
});
|
||||
}
|
||||
|
||||
private applyScopes(): void {
|
||||
this.canAuthor = this.auth.canAuthorPolicies?.() ?? false;
|
||||
this.canSimulate = this.auth.canSimulatePolicies?.() ?? false;
|
||||
|
||||
Reference in New Issue
Block a user