mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
5b9824c379
Co-authored-by: Pablo F.G <pablo.fernandez@prowler.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1053 lines
30 KiB
TypeScript
1053 lines
30 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
const providersActionsMock = vi.hoisted(() => ({
|
|
getProviders: vi.fn(),
|
|
getAllProviders: vi.fn(),
|
|
}));
|
|
|
|
const organizationsActionsMock = vi.hoisted(() => ({
|
|
listOrganizationsSafe: vi.fn(),
|
|
listOrganizationUnitsSafe: vi.fn(),
|
|
}));
|
|
|
|
const scansActionsMock = vi.hoisted(() => ({
|
|
getScans: vi.fn(),
|
|
}));
|
|
|
|
const schedulesActionsMock = vi.hoisted(() => ({
|
|
getSchedules: vi.fn(),
|
|
}));
|
|
|
|
const manageGroupsActionsMock = vi.hoisted(() => ({
|
|
getAllProviderGroups: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/actions/providers", () => providersActionsMock);
|
|
vi.mock(
|
|
"@/actions/organizations/organizations",
|
|
() => organizationsActionsMock,
|
|
);
|
|
vi.mock("@/actions/scans", () => scansActionsMock);
|
|
vi.mock("@/actions/schedules", () => schedulesActionsMock);
|
|
vi.mock("@/actions/manage-groups/manage-groups", () => manageGroupsActionsMock);
|
|
|
|
import { SearchParamsProps } from "@/types";
|
|
import { ProvidersApiResponse } from "@/types/providers";
|
|
import {
|
|
isProvidersOrganizationRow,
|
|
ProvidersProviderRow,
|
|
} from "@/types/providers-table";
|
|
import {
|
|
SCHEDULE_FREQUENCY,
|
|
type ScheduleAttributes,
|
|
type ScheduleProps,
|
|
} from "@/types/schedules";
|
|
|
|
import {
|
|
buildProvidersTableRows,
|
|
loadProvidersAccountsViewData,
|
|
PROVIDERS_ROW_TYPE,
|
|
} from "./providers-page.utils";
|
|
|
|
const providersResponse: ProvidersApiResponse = {
|
|
links: {
|
|
first: "",
|
|
last: "",
|
|
next: null,
|
|
prev: null,
|
|
},
|
|
data: [
|
|
{
|
|
id: "provider-1",
|
|
type: "providers",
|
|
attributes: {
|
|
provider: "aws",
|
|
uid: "111111111111",
|
|
alias: "AWS App Account",
|
|
status: "completed",
|
|
resources: 0,
|
|
connection: {
|
|
connected: true,
|
|
last_checked_at: "2025-02-13T11:17:00Z",
|
|
},
|
|
scanner_args: {
|
|
only_logs: false,
|
|
excluded_checks: [],
|
|
aws_retries_max_attempts: 3,
|
|
},
|
|
inserted_at: "2025-02-13T11:17:00Z",
|
|
updated_at: "2025-02-13T11:17:00Z",
|
|
created_by: {
|
|
object: "user",
|
|
id: "user-1",
|
|
},
|
|
},
|
|
relationships: {
|
|
secret: {
|
|
data: {
|
|
type: "provider-secrets",
|
|
id: "secret-1",
|
|
},
|
|
},
|
|
provider_groups: {
|
|
meta: {
|
|
count: 1,
|
|
},
|
|
data: [
|
|
{
|
|
type: "provider-groups",
|
|
id: "group-1",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: "provider-2",
|
|
type: "providers",
|
|
attributes: {
|
|
provider: "aws",
|
|
uid: "222222222222",
|
|
alias: "Standalone Account",
|
|
status: "completed",
|
|
resources: 0,
|
|
connection: {
|
|
connected: false,
|
|
last_checked_at: "2025-02-13T11:17:00Z",
|
|
},
|
|
scanner_args: {
|
|
only_logs: false,
|
|
excluded_checks: [],
|
|
aws_retries_max_attempts: 3,
|
|
},
|
|
inserted_at: "2025-02-13T11:17:00Z",
|
|
updated_at: "2025-02-13T11:17:00Z",
|
|
created_by: {
|
|
object: "user",
|
|
id: "user-1",
|
|
},
|
|
},
|
|
relationships: {
|
|
secret: {
|
|
data: null,
|
|
},
|
|
provider_groups: {
|
|
meta: {
|
|
count: 0,
|
|
},
|
|
data: [],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
included: [
|
|
{
|
|
type: "provider-groups",
|
|
id: "group-1",
|
|
attributes: {
|
|
name: "AWS Team",
|
|
},
|
|
},
|
|
],
|
|
meta: {
|
|
pagination: {
|
|
page: 1,
|
|
pages: 1,
|
|
count: 2,
|
|
},
|
|
version: "1",
|
|
},
|
|
};
|
|
|
|
const toProviderRow = (
|
|
provider: (typeof providersResponse.data)[number],
|
|
overrides?: Partial<ProvidersProviderRow>,
|
|
): ProvidersProviderRow => ({
|
|
...provider,
|
|
...overrides,
|
|
rowType: PROVIDERS_ROW_TYPE.PROVIDER,
|
|
groupNames: provider.id === "provider-1" ? ["AWS Team"] : [],
|
|
hasSchedule: false,
|
|
relationships: {
|
|
...provider.relationships,
|
|
...overrides?.relationships,
|
|
},
|
|
});
|
|
|
|
const buildSchedule = (
|
|
providerId: string,
|
|
overrides: Partial<ScheduleAttributes> = {},
|
|
): ScheduleProps => ({
|
|
type: "schedules",
|
|
id: providerId,
|
|
attributes: {
|
|
scan_enabled: true,
|
|
scan_frequency: SCHEDULE_FREQUENCY.DAILY,
|
|
scan_hour: 9,
|
|
scan_timezone: "Europe/Madrid",
|
|
scan_interval_hours: null,
|
|
scan_day_of_week: null,
|
|
scan_day_of_month: null,
|
|
...overrides,
|
|
},
|
|
relationships: {
|
|
provider: { data: { type: "providers", id: providerId } },
|
|
},
|
|
});
|
|
|
|
const findProviderRow = (
|
|
rows: { id: string }[],
|
|
providerId: string,
|
|
): ProvidersProviderRow | undefined =>
|
|
rows.find((row) => row.id === providerId) as ProvidersProviderRow | undefined;
|
|
|
|
describe("buildProvidersTableRows", () => {
|
|
it("returns a flat providers table for OSS", () => {
|
|
// Given
|
|
const providers = providersResponse.data.map((provider) =>
|
|
toProviderRow(provider),
|
|
);
|
|
|
|
// When
|
|
const rows = buildProvidersTableRows({
|
|
providers,
|
|
organizations: [],
|
|
organizationUnits: [],
|
|
isCloud: false,
|
|
});
|
|
|
|
// Then
|
|
expect(rows).toHaveLength(2);
|
|
expect(rows[0].rowType).toBe(PROVIDERS_ROW_TYPE.PROVIDER);
|
|
expect(rows[1].rowType).toBe(PROVIDERS_ROW_TYPE.PROVIDER);
|
|
});
|
|
|
|
it("nests providers under organizations and organization units in cloud", () => {
|
|
// Given
|
|
const providers = providersResponse.data.map((provider) =>
|
|
toProviderRow(provider, {
|
|
relationships: {
|
|
...provider.relationships,
|
|
organization: {
|
|
data:
|
|
provider.id === "provider-1"
|
|
? { type: "organizations", id: "org-1" }
|
|
: null,
|
|
},
|
|
organization_unit: {
|
|
data:
|
|
provider.id === "provider-1"
|
|
? { type: "organizational-units", id: "ou-1" }
|
|
: null,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
// When
|
|
const rows = buildProvidersTableRows({
|
|
providers,
|
|
organizations: [
|
|
{
|
|
id: "org-1",
|
|
type: "organizations",
|
|
attributes: {
|
|
name: "Root Organization",
|
|
org_type: "aws",
|
|
external_id: "o-root",
|
|
metadata: {},
|
|
root_external_id: "r-root",
|
|
},
|
|
relationships: {},
|
|
},
|
|
],
|
|
organizationUnits: [
|
|
{
|
|
id: "ou-1",
|
|
type: "organizational-units",
|
|
attributes: {
|
|
name: "Security OU",
|
|
external_id: "ou-security",
|
|
parent_external_id: "r-root",
|
|
metadata: {},
|
|
},
|
|
relationships: {
|
|
organization: {
|
|
data: {
|
|
type: "organizations",
|
|
id: "org-1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
isCloud: true,
|
|
});
|
|
|
|
// Then
|
|
expect(rows).toHaveLength(2);
|
|
expect(rows[0].rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION);
|
|
expect(rows[0].subRows).toHaveLength(1);
|
|
expect(rows[0].subRows?.[0].rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION);
|
|
expect(rows[0].subRows?.[0].subRows?.[0].rowType).toBe(
|
|
PROVIDERS_ROW_TYPE.PROVIDER,
|
|
);
|
|
expect(rows[1].rowType).toBe(PROVIDERS_ROW_TYPE.PROVIDER);
|
|
});
|
|
|
|
it("nests organizational units recursively up to multiple levels", () => {
|
|
// Given — OU hierarchy: org-1 > ou-root > ou-child > ou-grandchild
|
|
const providers = [
|
|
toProviderRow(providersResponse.data[0], {
|
|
relationships: {
|
|
...providersResponse.data[0].relationships,
|
|
organization: {
|
|
data: { type: "organizations", id: "org-1" },
|
|
},
|
|
organization_unit: {
|
|
data: { type: "organizational-units", id: "ou-grandchild" },
|
|
},
|
|
},
|
|
}),
|
|
];
|
|
|
|
// When
|
|
const rows = buildProvidersTableRows({
|
|
providers,
|
|
organizations: [
|
|
{
|
|
id: "org-1",
|
|
type: "organizations",
|
|
attributes: {
|
|
name: "Root Organization",
|
|
org_type: "aws",
|
|
external_id: "o-root",
|
|
metadata: {},
|
|
root_external_id: "r-root",
|
|
},
|
|
relationships: {},
|
|
},
|
|
],
|
|
organizationUnits: [
|
|
{
|
|
id: "ou-root",
|
|
type: "organizational-units",
|
|
attributes: {
|
|
name: "Production",
|
|
external_id: "ou-prod",
|
|
parent_external_id: "r-root",
|
|
metadata: {},
|
|
},
|
|
relationships: {
|
|
organization: {
|
|
data: { type: "organizations", id: "org-1" },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: "ou-child",
|
|
type: "organizational-units",
|
|
attributes: {
|
|
name: "EMEA",
|
|
external_id: "ou-emea",
|
|
parent_external_id: "ou-prod",
|
|
metadata: {},
|
|
},
|
|
relationships: {
|
|
organization: {
|
|
data: { type: "organizations", id: "org-1" },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: "ou-grandchild",
|
|
type: "organizational-units",
|
|
attributes: {
|
|
name: "Security",
|
|
external_id: "ou-security",
|
|
parent_external_id: "ou-emea",
|
|
metadata: {},
|
|
},
|
|
relationships: {
|
|
organization: {
|
|
data: { type: "organizations", id: "org-1" },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
isCloud: true,
|
|
});
|
|
|
|
// Then — org > ou-root > ou-child > ou-grandchild > provider
|
|
expect(rows).toHaveLength(1);
|
|
const orgRow = rows[0];
|
|
expect(orgRow.rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION);
|
|
expect(orgRow.subRows).toHaveLength(1);
|
|
|
|
const ouRoot = orgRow.subRows![0];
|
|
expect(ouRoot.rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION);
|
|
expect(ouRoot.subRows).toHaveLength(1);
|
|
|
|
const ouChild = ouRoot.subRows![0];
|
|
expect(ouChild.rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION);
|
|
expect(ouChild.subRows).toHaveLength(1);
|
|
|
|
const ouGrandchild = ouChild.subRows![0];
|
|
expect(ouGrandchild.rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION);
|
|
expect(ouGrandchild.subRows).toHaveLength(1);
|
|
expect(ouGrandchild.subRows![0].rowType).toBe(PROVIDERS_ROW_TYPE.PROVIDER);
|
|
});
|
|
|
|
it("nests providers under OUs using relationship-based parent IDs", () => {
|
|
// Given — providers have no org/OU linkage; tree is built from OU relationships
|
|
const providers = [toProviderRow(providersResponse.data[0])];
|
|
|
|
// When
|
|
const rows = buildProvidersTableRows({
|
|
providers,
|
|
organizations: [
|
|
{
|
|
id: "org-1",
|
|
type: "organizations",
|
|
attributes: {
|
|
name: "Root Organization",
|
|
org_type: "aws",
|
|
external_id: "o-root",
|
|
metadata: {},
|
|
root_external_id: "r-root",
|
|
},
|
|
relationships: {},
|
|
},
|
|
],
|
|
organizationUnits: [
|
|
{
|
|
id: "ou-parent",
|
|
type: "organizational-units",
|
|
attributes: {
|
|
name: "Workloads",
|
|
external_id: "ou-workloads",
|
|
parent_external_id: null,
|
|
metadata: {},
|
|
},
|
|
relationships: {
|
|
organization: {
|
|
data: { type: "organizations", id: "org-1" },
|
|
},
|
|
parent: {
|
|
data: null,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: "ou-child",
|
|
type: "organizational-units",
|
|
attributes: {
|
|
name: "Team A",
|
|
external_id: "ou-team-a",
|
|
parent_external_id: null,
|
|
metadata: {},
|
|
},
|
|
relationships: {
|
|
organization: {
|
|
data: { type: "organizations", id: "org-1" },
|
|
},
|
|
parent: {
|
|
data: { type: "organizational-units", id: "ou-parent" },
|
|
},
|
|
providers: {
|
|
data: [{ type: "providers", id: "provider-1" }],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
isCloud: true,
|
|
});
|
|
|
|
// Then — org > ou-parent > ou-child > provider
|
|
// Provider is claimed by ou-child via relationships, so org's direct
|
|
// providers list becomes empty and the org row only contains the OU subtree.
|
|
expect(rows).toHaveLength(1);
|
|
const orgRow = rows[0];
|
|
expect(orgRow.subRows).toHaveLength(1);
|
|
|
|
const ouParent = orgRow.subRows![0];
|
|
expect(ouParent.rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION);
|
|
expect(ouParent.subRows).toHaveLength(1);
|
|
|
|
const ouChild = ouParent.subRows![0];
|
|
expect(ouChild.rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION);
|
|
expect(ouChild.subRows).toHaveLength(1);
|
|
expect(ouChild.subRows![0].rowType).toBe(PROVIDERS_ROW_TYPE.PROVIDER);
|
|
});
|
|
|
|
it("does not duplicate providers that appear in both org relationships and OU assignments", () => {
|
|
// Given — provider-1 is linked to org-1 AND assigned to ou-1
|
|
const providers = [
|
|
toProviderRow(providersResponse.data[0], {
|
|
relationships: {
|
|
...providersResponse.data[0].relationships,
|
|
organization: {
|
|
data: { type: "organizations", id: "org-1" },
|
|
},
|
|
organization_unit: {
|
|
data: { type: "organizational-units", id: "ou-1" },
|
|
},
|
|
},
|
|
}),
|
|
];
|
|
|
|
// When
|
|
const rows = buildProvidersTableRows({
|
|
providers,
|
|
organizations: [
|
|
{
|
|
id: "org-1",
|
|
type: "organizations",
|
|
attributes: {
|
|
name: "Root Organization",
|
|
org_type: "aws",
|
|
external_id: "o-root",
|
|
metadata: {},
|
|
root_external_id: "r-root",
|
|
},
|
|
relationships: {
|
|
providers: {
|
|
data: [{ type: "providers", id: "provider-1" }],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
organizationUnits: [
|
|
{
|
|
id: "ou-1",
|
|
type: "organizational-units",
|
|
attributes: {
|
|
name: "Security OU",
|
|
external_id: "ou-security",
|
|
parent_external_id: "r-root",
|
|
metadata: {},
|
|
},
|
|
relationships: {
|
|
organization: {
|
|
data: { type: "organizations", id: "org-1" },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
isCloud: true,
|
|
});
|
|
|
|
// Then — provider appears only under OU, not duplicated at org level
|
|
expect(rows).toHaveLength(1);
|
|
const orgRow = rows[0];
|
|
expect(orgRow.rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION);
|
|
// Org should contain only the OU row, not the provider directly
|
|
expect(orgRow.subRows).toHaveLength(1);
|
|
expect(orgRow.subRows![0].rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION);
|
|
// The OU should contain the provider
|
|
expect(orgRow.subRows![0].subRows).toHaveLength(1);
|
|
expect(orgRow.subRows![0].subRows![0].rowType).toBe(
|
|
PROVIDERS_ROW_TYPE.PROVIDER,
|
|
);
|
|
expect(orgRow.subRows![0].subRows![0].id).toBe("provider-1");
|
|
});
|
|
|
|
it("keeps org-only providers as direct org children even when org has relationship data", () => {
|
|
// Given — provider-1 belongs to org-1 but has no OU
|
|
const providers = [
|
|
toProviderRow(providersResponse.data[0], {
|
|
relationships: {
|
|
...providersResponse.data[0].relationships,
|
|
organization: {
|
|
data: { type: "organizations", id: "org-1" },
|
|
},
|
|
organization_unit: {
|
|
data: null,
|
|
},
|
|
},
|
|
}),
|
|
];
|
|
|
|
// When
|
|
const rows = buildProvidersTableRows({
|
|
providers,
|
|
organizations: [
|
|
{
|
|
id: "org-1",
|
|
type: "organizations",
|
|
attributes: {
|
|
name: "Root Organization",
|
|
org_type: "aws",
|
|
external_id: "o-root",
|
|
metadata: {},
|
|
root_external_id: "r-root",
|
|
},
|
|
relationships: {
|
|
providers: {
|
|
data: [{ type: "providers", id: "provider-1" }],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
organizationUnits: [],
|
|
isCloud: true,
|
|
});
|
|
|
|
// Then — provider appears as a direct child of the org
|
|
expect(rows).toHaveLength(1);
|
|
const orgRow = rows[0];
|
|
expect(orgRow.rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION);
|
|
expect(orgRow.subRows).toHaveLength(1);
|
|
expect(orgRow.subRows![0].rowType).toBe(PROVIDERS_ROW_TYPE.PROVIDER);
|
|
expect(orgRow.subRows![0].id).toBe("provider-1");
|
|
});
|
|
|
|
it("groups providers from organization relationships when provider resources do not expose organization linkage", () => {
|
|
// Given
|
|
const providers = providersResponse.data.map((provider) =>
|
|
toProviderRow(provider, {
|
|
relationships: {
|
|
...provider.relationships,
|
|
organization: {
|
|
data: null,
|
|
},
|
|
organization_unit: {
|
|
data: null,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
// When
|
|
const rows = buildProvidersTableRows({
|
|
providers,
|
|
organizations: [
|
|
{
|
|
id: "org-1",
|
|
type: "organizations",
|
|
attributes: {
|
|
name: "Shared Organization",
|
|
org_type: "aws",
|
|
external_id: "o-shared",
|
|
metadata: {},
|
|
root_external_id: "r-shared",
|
|
},
|
|
relationships: {
|
|
providers: {
|
|
data: [
|
|
{ type: "providers", id: "provider-1" },
|
|
{ type: "providers", id: "provider-2" },
|
|
],
|
|
},
|
|
organizational_units: {
|
|
data: [],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
organizationUnits: [],
|
|
isCloud: true,
|
|
});
|
|
|
|
// Then
|
|
expect(rows).toHaveLength(1);
|
|
const orgRow = rows[0];
|
|
expect(isProvidersOrganizationRow(orgRow)).toBe(true);
|
|
if (!isProvidersOrganizationRow(orgRow)) {
|
|
throw new Error("Expected organization row");
|
|
}
|
|
expect(orgRow.subRows).toHaveLength(2);
|
|
expect(
|
|
orgRow.subRows?.every(
|
|
(row) => row.rowType === PROVIDERS_ROW_TYPE.PROVIDER,
|
|
),
|
|
).toBe(true);
|
|
expect(orgRow.providerIds).toEqual(["provider-1", "provider-2"]);
|
|
});
|
|
|
|
it("keeps organization relationship provider ids even when providers are not in the visible page", () => {
|
|
// Given
|
|
const providers = [
|
|
toProviderRow(providersResponse.data[0], {
|
|
relationships: {
|
|
...providersResponse.data[0].relationships,
|
|
organization: {
|
|
data: null,
|
|
},
|
|
},
|
|
}),
|
|
];
|
|
|
|
// When
|
|
const rows = buildProvidersTableRows({
|
|
providers,
|
|
organizations: [
|
|
{
|
|
id: "org-1",
|
|
type: "organizations",
|
|
attributes: {
|
|
name: "Large Organization",
|
|
org_type: "aws",
|
|
external_id: "o-large",
|
|
metadata: {},
|
|
root_external_id: "r-large",
|
|
},
|
|
relationships: {
|
|
providers: {
|
|
data: [
|
|
{ type: "providers", id: "provider-1" },
|
|
{ type: "providers", id: "provider-not-in-page" },
|
|
],
|
|
},
|
|
organizational_units: {
|
|
data: [],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
organizationUnits: [],
|
|
isCloud: true,
|
|
});
|
|
|
|
// Then
|
|
expect(rows).toHaveLength(1);
|
|
const orgRow = rows[0];
|
|
expect(isProvidersOrganizationRow(orgRow)).toBe(true);
|
|
if (!isProvidersOrganizationRow(orgRow)) {
|
|
throw new Error("Expected organization row");
|
|
}
|
|
expect(orgRow.subRows).toHaveLength(1);
|
|
expect(orgRow.providerIds).toEqual(["provider-1", "provider-not-in-page"]);
|
|
});
|
|
});
|
|
|
|
describe("loadProvidersAccountsViewData", () => {
|
|
it("does not call organizations endpoints in OSS", async () => {
|
|
// Given
|
|
providersActionsMock.getProviders.mockResolvedValue(providersResponse);
|
|
providersActionsMock.getAllProviders.mockResolvedValue(providersResponse);
|
|
scansActionsMock.getScans.mockResolvedValue({ data: [] });
|
|
|
|
// When
|
|
const viewData = await loadProvidersAccountsViewData({
|
|
searchParams: {} satisfies SearchParamsProps,
|
|
isCloud: false,
|
|
});
|
|
|
|
// Then
|
|
expect(
|
|
organizationsActionsMock.listOrganizationsSafe,
|
|
).not.toHaveBeenCalled();
|
|
expect(
|
|
organizationsActionsMock.listOrganizationUnitsSafe,
|
|
).not.toHaveBeenCalled();
|
|
expect(viewData.filters.map((filter) => filter.labelCheckboxGroup)).toEqual(
|
|
["Status"],
|
|
);
|
|
});
|
|
|
|
it("loads organizations filters and recursive rows in cloud", async () => {
|
|
// Given
|
|
providersActionsMock.getProviders.mockResolvedValue({
|
|
...providersResponse,
|
|
data: providersResponse.data.map((provider) => ({
|
|
...provider,
|
|
relationships: {
|
|
...provider.relationships,
|
|
organization: {
|
|
data:
|
|
provider.id === "provider-1"
|
|
? { type: "organizations", id: "org-1" }
|
|
: null,
|
|
},
|
|
organization_unit: {
|
|
data:
|
|
provider.id === "provider-1"
|
|
? { type: "organizational-units", id: "ou-1" }
|
|
: null,
|
|
},
|
|
},
|
|
})),
|
|
});
|
|
providersActionsMock.getAllProviders.mockResolvedValue(providersResponse);
|
|
organizationsActionsMock.listOrganizationsSafe.mockResolvedValue({
|
|
data: [
|
|
{
|
|
id: "org-1",
|
|
type: "organizations",
|
|
attributes: {
|
|
name: "Root Organization",
|
|
org_type: "aws",
|
|
external_id: "o-root",
|
|
metadata: {},
|
|
root_external_id: "r-root",
|
|
},
|
|
relationships: {},
|
|
},
|
|
],
|
|
});
|
|
organizationsActionsMock.listOrganizationUnitsSafe.mockResolvedValue({
|
|
data: [
|
|
{
|
|
id: "ou-1",
|
|
type: "organizational-units",
|
|
attributes: {
|
|
name: "Security OU",
|
|
external_id: "ou-security",
|
|
parent_external_id: "r-root",
|
|
metadata: {},
|
|
},
|
|
relationships: {
|
|
organization: {
|
|
data: {
|
|
type: "organizations",
|
|
id: "org-1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
scansActionsMock.getScans.mockResolvedValue({ data: [] });
|
|
|
|
// When
|
|
const viewData = await loadProvidersAccountsViewData({
|
|
searchParams: {} satisfies SearchParamsProps,
|
|
isCloud: true,
|
|
});
|
|
|
|
// Then
|
|
expect(
|
|
organizationsActionsMock.listOrganizationsSafe,
|
|
).toHaveBeenCalledTimes(1);
|
|
expect(
|
|
organizationsActionsMock.listOrganizationUnitsSafe,
|
|
).toHaveBeenCalledTimes(1);
|
|
expect(viewData.filters.map((filter) => filter.labelCheckboxGroup)).toEqual(
|
|
["Status"],
|
|
);
|
|
expect(viewData.rows[0].rowType).toBe(PROVIDERS_ROW_TYPE.ORGANIZATION);
|
|
});
|
|
|
|
it("falls back to empty cloud grouping data when organizations endpoints fail", async () => {
|
|
// Given
|
|
providersActionsMock.getProviders.mockResolvedValue(providersResponse);
|
|
providersActionsMock.getAllProviders.mockResolvedValue(providersResponse);
|
|
organizationsActionsMock.listOrganizationsSafe.mockResolvedValue({
|
|
data: [],
|
|
});
|
|
organizationsActionsMock.listOrganizationUnitsSafe.mockResolvedValue({
|
|
data: [],
|
|
});
|
|
scansActionsMock.getScans.mockResolvedValue({ data: [] });
|
|
|
|
// When
|
|
const viewData = await loadProvidersAccountsViewData({
|
|
searchParams: {} satisfies SearchParamsProps,
|
|
isCloud: true,
|
|
});
|
|
|
|
// Then
|
|
expect(viewData.filters.map((filter) => filter.labelCheckboxGroup)).toEqual(
|
|
["Status"],
|
|
);
|
|
expect(viewData.rows).toHaveLength(2);
|
|
expect(
|
|
viewData.rows.every((row) => row.rowType === PROVIDERS_ROW_TYPE.PROVIDER),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("surfaces the real cadence (not a hardcoded label) from a configured schedule with no materialized scan yet", async () => {
|
|
// Given — provider-1 has a WEEKLY schedule but the backend has not yet
|
|
// created a Scan row (the gap between configuring and the first fire).
|
|
providersActionsMock.getProviders.mockResolvedValue(providersResponse);
|
|
providersActionsMock.getAllProviders.mockResolvedValue(providersResponse);
|
|
scansActionsMock.getScans.mockResolvedValue({ data: [] });
|
|
schedulesActionsMock.getSchedules.mockResolvedValue({
|
|
data: [
|
|
buildSchedule("provider-1", {
|
|
scan_frequency: SCHEDULE_FREQUENCY.WEEKLY,
|
|
scan_hour: 9,
|
|
scan_day_of_week: 1,
|
|
}),
|
|
],
|
|
});
|
|
|
|
// When
|
|
const viewData = await loadProvidersAccountsViewData({
|
|
searchParams: {} satisfies SearchParamsProps,
|
|
isCloud: false,
|
|
});
|
|
|
|
// Then — the row carries the Weekly cadence, not "Daily".
|
|
const providerRow = findProviderRow(viewData.rows, "provider-1");
|
|
expect(providerRow?.hasSchedule).toBe(true);
|
|
expect(providerRow?.scheduleSummary?.cadence).toBe("Weekly on Monday");
|
|
expect(findProviderRow(viewData.rows, "provider-2")?.hasSchedule).toBe(
|
|
false,
|
|
);
|
|
expect(
|
|
findProviderRow(viewData.rows, "provider-2")?.scheduleSummary,
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("uses provider schedule attributes as authoritative when scan_hour is null", async () => {
|
|
// Given — provider-1 still has a materialized scheduled scan row, but the
|
|
// provider payload says the schedule was removed.
|
|
providersActionsMock.getProviders.mockResolvedValue({
|
|
...providersResponse,
|
|
data: [
|
|
{
|
|
...providersResponse.data[0],
|
|
attributes: {
|
|
...providersResponse.data[0].attributes,
|
|
scan_enabled: true,
|
|
scan_frequency: SCHEDULE_FREQUENCY.DAILY,
|
|
scan_hour: null,
|
|
scan_timezone: "UTC",
|
|
scan_interval_hours: null,
|
|
scan_day_of_week: null,
|
|
scan_day_of_month: null,
|
|
next_scan_at: null,
|
|
last_scan_at: null,
|
|
},
|
|
},
|
|
providersResponse.data[1],
|
|
],
|
|
});
|
|
providersActionsMock.getAllProviders.mockResolvedValue(providersResponse);
|
|
scansActionsMock.getScans.mockResolvedValue({
|
|
data: [
|
|
{
|
|
type: "scans",
|
|
id: "scan-1",
|
|
attributes: { trigger: "scheduled", state: "scheduled" },
|
|
relationships: {
|
|
provider: { data: { type: "providers", id: "provider-1" } },
|
|
},
|
|
},
|
|
],
|
|
});
|
|
schedulesActionsMock.getSchedules.mockResolvedValue({
|
|
data: [buildSchedule("provider-1", { scan_hour: 9 })],
|
|
});
|
|
|
|
// When
|
|
const viewData = await loadProvidersAccountsViewData({
|
|
searchParams: {} satisfies SearchParamsProps,
|
|
isCloud: false,
|
|
});
|
|
|
|
// Then
|
|
const providerRow = findProviderRow(viewData.rows, "provider-1");
|
|
expect(providerRow?.hasSchedule).toBe(false);
|
|
expect(providerRow?.scheduleSummary).toBeUndefined();
|
|
expect(providerRow?.lastScanAt).toBeNull();
|
|
});
|
|
|
|
it("builds provider schedule and last scan values from the provider payload", async () => {
|
|
// Given
|
|
providersActionsMock.getProviders.mockResolvedValue({
|
|
...providersResponse,
|
|
data: [
|
|
{
|
|
...providersResponse.data[0],
|
|
attributes: {
|
|
...providersResponse.data[0].attributes,
|
|
scan_enabled: true,
|
|
scan_frequency: SCHEDULE_FREQUENCY.MONTHLY,
|
|
scan_hour: 8,
|
|
scan_timezone: "Europe/Madrid",
|
|
scan_interval_hours: null,
|
|
scan_day_of_week: null,
|
|
scan_day_of_month: 24,
|
|
next_scan_at: "2026-06-24T06:00:00Z",
|
|
last_scan_at: "2026-06-23T06:00:00Z",
|
|
},
|
|
},
|
|
providersResponse.data[1],
|
|
],
|
|
});
|
|
providersActionsMock.getAllProviders.mockResolvedValue(providersResponse);
|
|
scansActionsMock.getScans.mockResolvedValue({ data: [] });
|
|
schedulesActionsMock.getSchedules.mockResolvedValue({ error: "Not found" });
|
|
|
|
// When
|
|
const viewData = await loadProvidersAccountsViewData({
|
|
searchParams: {} satisfies SearchParamsProps,
|
|
isCloud: false,
|
|
});
|
|
|
|
// Then
|
|
const providerRow = findProviderRow(viewData.rows, "provider-1");
|
|
expect(providerRow?.hasSchedule).toBe(true);
|
|
expect(providerRow?.scheduleSummary?.cadence).toBe("Monthly on the 24th");
|
|
expect(providerRow?.scheduleSummary?.nextScanAt).toBe(
|
|
"2026-06-24T06:00:00Z",
|
|
);
|
|
expect(providerRow?.lastScanAt).toBe("2026-06-23T06:00:00Z");
|
|
});
|
|
|
|
it("ignores paused or unconfigured schedules", async () => {
|
|
// Given — provider-1 paused (disabled), provider-2 never configured.
|
|
providersActionsMock.getProviders.mockResolvedValue(providersResponse);
|
|
providersActionsMock.getAllProviders.mockResolvedValue(providersResponse);
|
|
scansActionsMock.getScans.mockResolvedValue({ data: [] });
|
|
schedulesActionsMock.getSchedules.mockResolvedValue({
|
|
data: [
|
|
buildSchedule("provider-1", { scan_enabled: false, scan_hour: 9 }),
|
|
buildSchedule("provider-2", { scan_enabled: true, scan_hour: null }),
|
|
],
|
|
});
|
|
|
|
// When
|
|
const viewData = await loadProvidersAccountsViewData({
|
|
searchParams: {} satisfies SearchParamsProps,
|
|
isCloud: false,
|
|
});
|
|
|
|
// Then
|
|
expect(findProviderRow(viewData.rows, "provider-1")?.hasSchedule).toBe(
|
|
false,
|
|
);
|
|
expect(findProviderRow(viewData.rows, "provider-2")?.hasSchedule).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it("does not infer provider schedules from materialized scans when /schedules is unavailable", async () => {
|
|
// Given — /schedules errors, and provider-1 still has a materialized
|
|
// scheduled scan. That scan is historical execution state, not schedule
|
|
// configuration.
|
|
providersActionsMock.getProviders.mockResolvedValue(providersResponse);
|
|
providersActionsMock.getAllProviders.mockResolvedValue(providersResponse);
|
|
scansActionsMock.getScans.mockResolvedValue({
|
|
data: [
|
|
{
|
|
type: "scans",
|
|
id: "scan-1",
|
|
attributes: { trigger: "scheduled", state: "scheduled" },
|
|
relationships: {
|
|
provider: { data: { type: "providers", id: "provider-1" } },
|
|
},
|
|
},
|
|
],
|
|
});
|
|
schedulesActionsMock.getSchedules.mockResolvedValue({ error: "Not found" });
|
|
|
|
// When
|
|
const viewData = await loadProvidersAccountsViewData({
|
|
searchParams: {} satisfies SearchParamsProps,
|
|
isCloud: false,
|
|
});
|
|
|
|
// Then — only provider scan_* fields or /schedules can mark a schedule.
|
|
expect(findProviderRow(viewData.rows, "provider-1")?.hasSchedule).toBe(
|
|
false,
|
|
);
|
|
expect(findProviderRow(viewData.rows, "provider-2")?.hasSchedule).toBe(
|
|
false,
|
|
);
|
|
});
|
|
});
|