Files
prowler/ui/lib/provider-helpers.test.ts
T

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",
});
});
});