mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Ungate reporting catalog metadata
This commit is contained in:
parent
9cc3ff11f8
commit
899466d13a
10 changed files with 77 additions and 5 deletions
|
|
@ -181,6 +181,11 @@ plus a VM inventory definition route that own panel copy, performance report
|
|||
options, export title, column schema, and filename prefixes. Lifecycle-
|
||||
adjacent install and fleet surfaces may read those facts, but they must not
|
||||
redefine reporting or inventory schema locally.
|
||||
That catalog route is intentionally metadata-readable without the
|
||||
`advanced_reporting` feature gate so locked admin reporting shells can stay on
|
||||
the same API-owned definition before upsell; lifecycle-adjacent surfaces must
|
||||
not treat that metadata visibility as permission to execute paid report/export
|
||||
routes.
|
||||
That same API-owned performance-report definition also governs transport-side
|
||||
validation and attachment naming. Lifecycle-adjacent fleet surfaces may depend
|
||||
on those downloads, but they must treat allowed formats, multi-resource caps,
|
||||
|
|
|
|||
|
|
@ -242,6 +242,10 @@ consumers may validate or reject malformed payloads, but they must not invent
|
|||
replacement report endpoints, filename prefixes, export routes, or default
|
||||
range windows from frontend-local fallback constants once the catalog has been
|
||||
accepted.
|
||||
The catalog route itself is intentionally metadata-readable without the
|
||||
`advanced_reporting` feature gate so locked admin surfaces can present the same
|
||||
canonical reporting definition before upsell, while report generation and
|
||||
inventory export remain feature-gated execution routes.
|
||||
The `/api/resources` serializer now also refreshes canonical identity and
|
||||
policy metadata through the shared unified-resource helper before it writes
|
||||
the payload, so backend and frontend contract tests stay aligned on one
|
||||
|
|
|
|||
|
|
@ -199,6 +199,9 @@ format/range selections from legacy frontend constants or invent fallback
|
|||
report endpoints, filename prefixes, or range windows when the catalog is
|
||||
present; the first valid selection and all request semantics must come from the
|
||||
parsed backend definition.
|
||||
That same settings shell must also read the reporting catalog for locked users,
|
||||
not just entitled users, so the paywalled reporting panel does not drift onto a
|
||||
separate frontend-owned title or description contract.
|
||||
The shared updates settings owner also defines the user-facing framing for
|
||||
rc-tagged builds. `frontend-modern/src/components/Settings/updatesSettingsModel.ts`
|
||||
and `frontend-modern/src/utils/updatesPresentation.ts` must present that
|
||||
|
|
|
|||
|
|
@ -227,6 +227,11 @@ plus a VM inventory definition route that own panel copy, stable column
|
|||
schema, and filename prefix. Storage and recovery flows may read those facts
|
||||
when they need fleet context, but they must not fork their own reporting or
|
||||
inventory column contract.
|
||||
That catalog route is intentionally metadata-readable without the
|
||||
`advanced_reporting` feature gate so locked admin reporting shells can stay on
|
||||
the same API-owned definition before upsell; storage- and recovery-adjacent
|
||||
surfaces must not treat that metadata visibility as permission to execute paid
|
||||
report/export routes.
|
||||
That same API-owned performance-report definition also governs transport-side
|
||||
validation and attachment naming. Storage and recovery flows may consume those
|
||||
downloads, but they must treat allowed formats, multi-resource caps, optional
|
||||
|
|
|
|||
|
|
@ -1097,6 +1097,8 @@ describe('Settings architecture guardrails', () => {
|
|||
expect(reportingPanelStateSource).toContain('buildReportingRequest');
|
||||
expect(reportingPanelStateSource).toContain('buildReportingCatalogRequest');
|
||||
expect(reportingPanelStateSource).toContain('parseReportingCatalog');
|
||||
expect(reportingPanelStateSource).toContain('!licenseLoaded()');
|
||||
expect(reportingPanelStateSource).not.toContain('!isReportingEnabled()');
|
||||
expect(reportingPanelStateSource).toContain('buildVMInventoryExportRequest');
|
||||
expect(reportingPanelStateSource).toContain('getReportingGenerateSuccessMessage');
|
||||
expect(reportingPanelStateSource).not.toContain('getTrialAlreadyUsedMessage()');
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ const catalogPayload = {
|
|||
describe('useReportingPanelState', () => {
|
||||
let useReportingPanelState: UseReportingPanelStateModule['useReportingPanelState'];
|
||||
let apiFetchMock: ReturnType<typeof vi.fn>;
|
||||
let hasReportingFeature: boolean;
|
||||
let loadLicenseStatusMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
|
@ -74,6 +75,7 @@ describe('useReportingPanelState', () => {
|
|||
apiFetchMock = vi
|
||||
.fn()
|
||||
.mockResolvedValue(new Response(JSON.stringify(catalogPayload), { status: 200 }));
|
||||
hasReportingFeature = true;
|
||||
loadLicenseStatusMock = vi.fn();
|
||||
|
||||
vi.doMock('@/utils/apiClient', () => ({
|
||||
|
|
@ -96,7 +98,7 @@ describe('useReportingPanelState', () => {
|
|||
vi.doMock('@/stores/license', () => ({
|
||||
entitlements: vi.fn(() => ({ trial_eligible: true })),
|
||||
getUpgradeActionUrlOrFallback: vi.fn(() => '/pricing'),
|
||||
hasFeature: vi.fn((feature: string) => feature === 'advanced_reporting'),
|
||||
hasFeature: vi.fn((feature: string) => feature === 'advanced_reporting' && hasReportingFeature),
|
||||
licenseLoaded: vi.fn(() => true),
|
||||
loadLicenseStatus: loadLicenseStatusMock,
|
||||
}));
|
||||
|
|
@ -135,4 +137,17 @@ describe('useReportingPanelState', () => {
|
|||
|
||||
dispose();
|
||||
});
|
||||
|
||||
it('loads the reporting catalog even when the feature is locked', async () => {
|
||||
hasReportingFeature = false;
|
||||
const { hookState, dispose } = mountHook();
|
||||
|
||||
await flushAsync();
|
||||
await flushAsync();
|
||||
|
||||
expect(apiFetchMock).toHaveBeenCalledWith('/api/admin/reports/catalog');
|
||||
expect(hookState.reportingCatalog()?.title).toBe('Detailed Reporting');
|
||||
|
||||
dispose();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export const useReportingPanelState = () => {
|
|||
|
||||
createEffect(() => {
|
||||
if (
|
||||
!isReportingEnabled() ||
|
||||
!licenseLoaded() ||
|
||||
reportingCatalog() ||
|
||||
reportingCatalogLoading() ||
|
||||
reportingCatalogRequested()
|
||||
|
|
|
|||
|
|
@ -499,6 +499,25 @@ func TestContract_PerformanceReportTransportUsesCatalogDefinition(t *testing.T)
|
|||
}
|
||||
}
|
||||
|
||||
func TestContract_ReportingCatalogRouteAccessibleWithoutReportingFeature(t *testing.T) {
|
||||
rawToken := "reporting-catalog-contract-token-123.12345678"
|
||||
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
||||
cfg := newTestConfigWithTokens(t, record)
|
||||
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/admin/reports/catalog", nil)
|
||||
req.Header.Set("X-API-Token", rawToken)
|
||||
rec := httptest.NewRecorder()
|
||||
router.Handler().ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 for ungated reporting catalog route, got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if got := rec.Header().Get("Content-Type"); !strings.Contains(got, "application/json") {
|
||||
t.Fatalf("expected json content type, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContract_PerformanceReportTransportUsesCatalogDefaultRange(t *testing.T) {
|
||||
engine := &stubReportingEngine{data: []byte("report"), contentType: "application/pdf"}
|
||||
original := reporting.GetEngine()
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ func (r *Router) registerOrgLicenseRoutesGroup(orgHandlers *OrgHandlers, rbacHan
|
|||
if !ensureAdminSession(r.config, w, req) {
|
||||
return
|
||||
}
|
||||
RequireLicenseFeature(r.licenseHandlers, featureAdvancedReportingValue, RequireScope(config.ScopeSettingsRead, reportingAdminEndpoints.HandleGetReportingCatalog))(w, req)
|
||||
RequireScope(config.ScopeSettingsRead, reportingAdminEndpoints.HandleGetReportingCatalog)(w, req)
|
||||
}))
|
||||
r.mux.HandleFunc("/api/admin/reports/generate", RequirePermission(r.config, r.authorizer, auth.ActionRead, auth.ResourceNodes, func(w http.ResponseWriter, req *http.Request) {
|
||||
if !ensureAdminSession(r.config, w, req) {
|
||||
|
|
|
|||
|
|
@ -2893,20 +2893,39 @@ func TestAuditVerifyRequiresLicenseFeature(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestReportingEndpointsRequireLicenseFeature(t *testing.T) {
|
||||
func TestReportingCatalogDoesNotRequireLicenseFeature(t *testing.T) {
|
||||
rawToken := "reporting-license-token-123.12345678"
|
||||
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
||||
cfg := newTestConfigWithTokens(t, record)
|
||||
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/admin/reports/catalog", nil)
|
||||
req.Header.Set("X-API-Token", rawToken)
|
||||
rec := httptest.NewRecorder()
|
||||
router.Handler().ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 for reporting catalog without reporting license, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportingExecutionEndpointsRequireLicenseFeature(t *testing.T) {
|
||||
rawToken := "reporting-license-token-123.12345678"
|
||||
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
||||
cfg := newTestConfigWithTokens(t, record)
|
||||
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
||||
|
||||
paths := []string{
|
||||
"/api/admin/reports/catalog",
|
||||
"/api/admin/reports/generate",
|
||||
"/api/admin/reports/generate-multi",
|
||||
"/api/admin/reports/inventory/vms/definition",
|
||||
"/api/admin/reports/inventory/vms/export",
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
if path == "/api/admin/reports/generate-multi" {
|
||||
req = httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{}`))
|
||||
}
|
||||
req.Header.Set("X-API-Token", rawToken)
|
||||
rec := httptest.NewRecorder()
|
||||
router.Handler().ServeHTTP(rec, req)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue