mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
369 lines
12 KiB
TypeScript
369 lines
12 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const { checkConnectionProvider, checkTaskStatus, getProvider } = vi.hoisted(
|
|
() => ({
|
|
checkConnectionProvider: vi.fn(),
|
|
checkTaskStatus: vi.fn(),
|
|
getProvider: vi.fn(),
|
|
}),
|
|
);
|
|
vi.mock("@/actions/providers/providers", () => ({
|
|
checkConnectionProvider,
|
|
getProvider,
|
|
}));
|
|
vi.mock("./helper", () => ({
|
|
checkTaskStatus,
|
|
TASK_STATUS_MAX_RETRIES_ERROR: "Max retries exceeded",
|
|
}));
|
|
|
|
import {
|
|
CONNECTION_CHECK_STATUS,
|
|
PROVIDER_CONNECTION_CHECK_MAX_RETRIES,
|
|
PROVIDER_CONNECTION_CHECK_POLL_DELAY_MS,
|
|
resolveProviderConnectionState,
|
|
testProviderConnection,
|
|
} from "./provider-helpers";
|
|
|
|
describe("resolveProviderConnectionState", () => {
|
|
it("trusts a stored connected=true once last_checked_at differs from the baseline", async () => {
|
|
getProvider.mockResolvedValue({
|
|
data: {
|
|
attributes: {
|
|
connection: {
|
|
connected: true,
|
|
last_checked_at: "2026-01-01T00:00:10.000Z",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(await resolveProviderConnectionState("account", null)).toEqual({
|
|
status: CONNECTION_CHECK_STATUS.SUCCESS,
|
|
error: null,
|
|
});
|
|
});
|
|
|
|
it("treats a stored connected=true as pending while last_checked_at still matches the baseline", async () => {
|
|
// Given: the provider was connected from a previous check, and this check --
|
|
// dispatched after that baseline was captured -- is still running.
|
|
const baseline = "2025-12-31T23:59:59.000Z";
|
|
getProvider.mockResolvedValue({
|
|
data: {
|
|
attributes: {
|
|
connection: { connected: true, last_checked_at: baseline },
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await resolveProviderConnectionState("account", baseline);
|
|
|
|
expect(result.status).toBe(CONNECTION_CHECK_STATUS.PENDING);
|
|
expect(result.error).toMatch(/still running/i);
|
|
});
|
|
|
|
it("reports success once last_checked_at changes, even when the new value sorts earlier than the baseline", async () => {
|
|
// Given: a deployment whose clocks are not synchronised -- the server writes
|
|
// a `last_checked_at` that, read as a plain string/date, sorts *before* the
|
|
// baseline captured moments earlier from the same server. A timestamp
|
|
// comparison would misread this as stale; a value comparison does not.
|
|
const baseline = "2026-06-01T00:00:00.000Z";
|
|
getProvider.mockResolvedValue({
|
|
data: {
|
|
attributes: {
|
|
connection: {
|
|
connected: true,
|
|
last_checked_at: "2020-01-01T00:00:00.000Z",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await resolveProviderConnectionState("account", baseline);
|
|
|
|
expect(result).toEqual({
|
|
status: CONNECTION_CHECK_STATUS.SUCCESS,
|
|
error: null,
|
|
});
|
|
});
|
|
|
|
it("treats a missing last_checked_at as pending even when connected is true", async () => {
|
|
getProvider.mockResolvedValue({
|
|
data: {
|
|
attributes: {
|
|
connection: { connected: true, last_checked_at: null },
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await resolveProviderConnectionState("account", null);
|
|
|
|
expect(result.status).toBe(CONNECTION_CHECK_STATUS.PENDING);
|
|
});
|
|
|
|
it("treats an unknown baseline as unresolved, never trusting a pre-existing stored result", async () => {
|
|
// Given: the pre-dispatch baseline read failed, so there is nothing to
|
|
// compare this result against.
|
|
getProvider.mockResolvedValue({
|
|
data: {
|
|
attributes: {
|
|
connection: {
|
|
connected: true,
|
|
last_checked_at: "2026-01-01T00:00:10.000Z",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await resolveProviderConnectionState("account", undefined);
|
|
|
|
expect(result.status).toBe(CONNECTION_CHECK_STATUS.PENDING);
|
|
});
|
|
|
|
it("reports a changed connected=false as a confirmed failure", async () => {
|
|
getProvider.mockResolvedValue({
|
|
data: {
|
|
attributes: {
|
|
connection: {
|
|
connected: false,
|
|
last_checked_at: "2026-01-01T00:00:10.000Z",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await resolveProviderConnectionState("account", null);
|
|
|
|
expect(result).toEqual({
|
|
status: CONNECTION_CHECK_STATUS.FAILED,
|
|
error: expect.stringMatching(/test the connection again/i),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("provider connection confirmation", () => {
|
|
beforeEach(() => {
|
|
// The baseline read (before dispatch) and the exhausted-wait fallback read
|
|
// both go through `getProvider`; default to "no prior check" unless a test
|
|
// overrides it.
|
|
getProvider.mockResolvedValue({
|
|
data: {
|
|
attributes: { connection: { connected: null, last_checked_at: null } },
|
|
},
|
|
});
|
|
checkConnectionProvider.mockResolvedValue({ data: { id: "task" } });
|
|
});
|
|
it.each([undefined, {}, { connected: "true" }, { connected: false }])(
|
|
"does not advance without explicit connected=true: %j",
|
|
async (result) => {
|
|
checkTaskStatus.mockResolvedValue({
|
|
completed: true,
|
|
task: { data: { attributes: { result } } },
|
|
});
|
|
expect((await testProviderConnection("account")).status).toBe(
|
|
CONNECTION_CHECK_STATUS.FAILED,
|
|
);
|
|
},
|
|
);
|
|
it("advances on an explicitly successful connection", async () => {
|
|
checkTaskStatus.mockResolvedValue({
|
|
completed: true,
|
|
task: { data: { attributes: { result: { connected: true } } } },
|
|
});
|
|
expect(await testProviderConnection("account")).toEqual({
|
|
status: CONNECTION_CHECK_STATUS.SUCCESS,
|
|
error: null,
|
|
});
|
|
});
|
|
|
|
it("reads the baseline before dispatching the check", async () => {
|
|
checkTaskStatus.mockResolvedValue({
|
|
completed: true,
|
|
task: { data: { attributes: { result: { connected: true } } } },
|
|
});
|
|
|
|
const callOrder: string[] = [];
|
|
getProvider.mockImplementation(async () => {
|
|
callOrder.push("getProvider");
|
|
return {
|
|
data: {
|
|
attributes: {
|
|
connection: { connected: null, last_checked_at: null },
|
|
},
|
|
},
|
|
};
|
|
});
|
|
checkConnectionProvider.mockImplementation(async () => {
|
|
callOrder.push("checkConnectionProvider");
|
|
return { data: { id: "task" } };
|
|
});
|
|
|
|
await testProviderConnection("account");
|
|
|
|
expect(callOrder).toEqual(["getProvider", "checkConnectionProvider"]);
|
|
});
|
|
|
|
it("sizes the wait past the backend's 120s provider-connection-check time limit", async () => {
|
|
checkTaskStatus.mockResolvedValue({
|
|
completed: true,
|
|
task: { data: { attributes: { result: { connected: true } } } },
|
|
});
|
|
|
|
await testProviderConnection("account");
|
|
|
|
expect(checkTaskStatus).toHaveBeenCalledWith(
|
|
"task",
|
|
PROVIDER_CONNECTION_CHECK_MAX_RETRIES,
|
|
PROVIDER_CONNECTION_CHECK_POLL_DELAY_MS,
|
|
);
|
|
expect(
|
|
PROVIDER_CONNECTION_CHECK_MAX_RETRIES *
|
|
PROVIDER_CONNECTION_CHECK_POLL_DELAY_MS,
|
|
).toBeGreaterThan(120_000);
|
|
});
|
|
|
|
describe("when the wait is exhausted", () => {
|
|
beforeEach(() => {
|
|
checkTaskStatus.mockResolvedValue({
|
|
completed: false,
|
|
error: "Max retries exceeded",
|
|
});
|
|
});
|
|
|
|
it("reports success when the stored state changed after the baseline was captured", async () => {
|
|
// Baseline (read before dispatch): no prior check. Fallback read: a fresh
|
|
// result landed while the UI was waiting.
|
|
getProvider
|
|
.mockResolvedValueOnce({
|
|
data: {
|
|
attributes: {
|
|
connection: { connected: null, last_checked_at: null },
|
|
},
|
|
},
|
|
})
|
|
.mockResolvedValueOnce({
|
|
data: {
|
|
attributes: {
|
|
connection: {
|
|
connected: true,
|
|
last_checked_at: "2999-01-01T00:00:00Z",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(await testProviderConnection("account")).toEqual({
|
|
status: CONNECTION_CHECK_STATUS.SUCCESS,
|
|
error: null,
|
|
});
|
|
});
|
|
|
|
it("does not report success from a stale stored result predating this check", async () => {
|
|
// Given: the provider was already connected from a previous check --
|
|
// captured as the baseline before this check was dispatched -- and the
|
|
// fallback read comes back with that exact same (unchanged) value,
|
|
// meaning the new check has not written a result yet.
|
|
const priorLastCheckedAt = "2025-06-01T00:00:00Z";
|
|
getProvider.mockResolvedValue({
|
|
data: {
|
|
attributes: {
|
|
connection: {
|
|
connected: true,
|
|
last_checked_at: priorLastCheckedAt,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await testProviderConnection("account");
|
|
|
|
expect(result.status).not.toBe(CONNECTION_CHECK_STATUS.SUCCESS);
|
|
expect(result.status).toBe(CONNECTION_CHECK_STATUS.PENDING);
|
|
});
|
|
|
|
it("reports pending, not connected, when the browser clock is behind the server and the stored result predates the check", async () => {
|
|
// Given: a deployment whose clocks are not synchronised -- the browser's
|
|
// clock is behind the server's. Under a clock-based comparison this could
|
|
// make an older stored result look newer than the check's start and be
|
|
// reported as connected. The baseline here is captured from the server's
|
|
// own prior value, so the comparison never depends on the browser's clock
|
|
// at all: an unchanged value is still unchanged.
|
|
const staleServerTimestamp = "2026-03-01T12:00:00Z";
|
|
getProvider.mockResolvedValue({
|
|
data: {
|
|
attributes: {
|
|
connection: {
|
|
connected: true,
|
|
last_checked_at: staleServerTimestamp,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await testProviderConnection("account");
|
|
|
|
expect(result.status).toBe(CONNECTION_CHECK_STATUS.PENDING);
|
|
expect(result.status).not.toBe(CONNECTION_CHECK_STATUS.SUCCESS);
|
|
});
|
|
|
|
it("reports the failure when the provider is confirmed not connected by this check", async () => {
|
|
getProvider
|
|
.mockResolvedValueOnce({
|
|
data: {
|
|
attributes: {
|
|
connection: { connected: null, last_checked_at: null },
|
|
},
|
|
},
|
|
})
|
|
.mockResolvedValueOnce({
|
|
data: {
|
|
attributes: {
|
|
connection: {
|
|
connected: false,
|
|
last_checked_at: "2999-01-01T00:00:00Z",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await testProviderConnection("account");
|
|
|
|
expect(result.status).toBe(CONNECTION_CHECK_STATUS.FAILED);
|
|
expect(result.error).toMatch(/test the connection again/i);
|
|
});
|
|
|
|
it("shows a neutral still-checking message when the provider state is undetermined", async () => {
|
|
getProvider.mockResolvedValue({
|
|
data: {
|
|
attributes: {
|
|
connection: { connected: null, last_checked_at: null },
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await testProviderConnection("account");
|
|
|
|
expect(result.status).toBe(CONNECTION_CHECK_STATUS.PENDING);
|
|
expect(result.error).toMatch(/still running|refresh/i);
|
|
expect(result.error?.toLowerCase()).not.toContain("error");
|
|
expect(result.error?.toLowerCase()).not.toContain("failed");
|
|
});
|
|
});
|
|
|
|
it("does not re-read provider state for a real task failure", async () => {
|
|
checkTaskStatus.mockResolvedValue({
|
|
completed: false,
|
|
error: "Unexpected task state",
|
|
});
|
|
|
|
const result = await testProviderConnection("account");
|
|
|
|
// The baseline read (before dispatch) still happens, but nothing reads
|
|
// provider state again to resolve this failure.
|
|
expect(getProvider).toHaveBeenCalledTimes(1);
|
|
expect(result).toEqual({
|
|
status: CONNECTION_CHECK_STATUS.FAILED,
|
|
error: "Unexpected task state",
|
|
});
|
|
});
|
|
});
|