mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
312 lines
10 KiB
TypeScript
312 lines
10 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import type { RegistryCredentialStatus } from "@/types/registry";
|
|
|
|
import { executeRegistryCredentialValidation } from "./credential-execution";
|
|
import { REGISTRY_CREDENTIAL_TASK_KIND } from "./credential-task";
|
|
|
|
const {
|
|
refreshRegistryCollectionsMock,
|
|
refreshRegistryCredentialMock,
|
|
submitRegistryCredentialMock,
|
|
trackAndPollTaskMock,
|
|
} = vi.hoisted(() => ({
|
|
refreshRegistryCollectionsMock: vi.fn(),
|
|
refreshRegistryCredentialMock: vi.fn(),
|
|
submitRegistryCredentialMock: vi.fn(),
|
|
trackAndPollTaskMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/actions/registry/registry", () => ({
|
|
refreshRegistryCollections: refreshRegistryCollectionsMock,
|
|
refreshRegistryCredential: refreshRegistryCredentialMock,
|
|
submitRegistryCredential: submitRegistryCredentialMock,
|
|
}));
|
|
|
|
vi.mock("@/store/task-watcher/store", () => ({
|
|
TASK_WATCHER_STATUS: { PENDING: "pending", READY: "ready", ERROR: "error" },
|
|
trackAndPollTask: trackAndPollTaskMock,
|
|
}));
|
|
|
|
const activeCredential: RegistryCredentialStatus = {
|
|
configured: true,
|
|
isValid: true,
|
|
scopes: ["catalog:read"],
|
|
validationPending: false,
|
|
};
|
|
const noCredential: RegistryCredentialStatus = {
|
|
configured: false,
|
|
isValid: false,
|
|
scopes: [],
|
|
validationPending: false,
|
|
};
|
|
const pendingCredential: RegistryCredentialStatus = {
|
|
configured: true,
|
|
isValid: false,
|
|
scopes: [],
|
|
validationPending: true,
|
|
};
|
|
|
|
describe("executeRegistryCredentialValidation", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
refreshRegistryCollectionsMock.mockResolvedValue({
|
|
status: "complete",
|
|
catalog: { status: "complete", artifacts: [] },
|
|
tenantArtifacts: [],
|
|
});
|
|
submitRegistryCredentialMock.mockResolvedValue({
|
|
status: "submitted",
|
|
taskId: "task-1",
|
|
priorConfigured: false,
|
|
});
|
|
trackAndPollTaskMock.mockResolvedValue({
|
|
status: "ready",
|
|
result: { stored: true, error: null },
|
|
});
|
|
refreshRegistryCredentialMock.mockResolvedValue({
|
|
status: "status",
|
|
credential: activeCredential,
|
|
});
|
|
});
|
|
|
|
it("connects after the watched task settles and the credential is active", async () => {
|
|
// Given
|
|
const key = "registry-test-key";
|
|
|
|
// When
|
|
const outcome = await executeRegistryCredentialValidation(key);
|
|
|
|
// Then
|
|
expect(outcome).toEqual({
|
|
status: "connected",
|
|
credential: activeCredential,
|
|
collections: {
|
|
status: "complete",
|
|
catalog: { status: "complete", artifacts: [] },
|
|
tenantArtifacts: [],
|
|
},
|
|
});
|
|
expect(submitRegistryCredentialMock).toHaveBeenCalledWith(key);
|
|
expect(refreshRegistryCredentialMock).toHaveBeenCalledTimes(1);
|
|
expect(refreshRegistryCollectionsMock).toHaveBeenCalledTimes(1);
|
|
expect(trackAndPollTaskMock).toHaveBeenCalledWith({
|
|
taskId: "task-1",
|
|
kind: REGISTRY_CREDENTIAL_TASK_KIND,
|
|
meta: { priorConfigured: "false" },
|
|
notifyHandler: false,
|
|
});
|
|
// The key must never reach the persisted watcher record.
|
|
expect(JSON.stringify(trackAndPollTaskMock.mock.calls)).not.toContain(key);
|
|
});
|
|
|
|
it("reports an invalid key when the settled credential is not active", async () => {
|
|
// Given
|
|
refreshRegistryCredentialMock.mockResolvedValue({
|
|
status: "status",
|
|
credential: noCredential,
|
|
});
|
|
|
|
// When
|
|
const outcome = await executeRegistryCredentialValidation("bad-key");
|
|
|
|
// Then
|
|
expect(outcome).toEqual({ status: "invalid", credential: noCredential });
|
|
});
|
|
|
|
it("keeps a failed replacement distinct from a first invalid key", async () => {
|
|
// Given
|
|
submitRegistryCredentialMock.mockResolvedValue({
|
|
status: "submitted",
|
|
taskId: "task-2",
|
|
priorConfigured: true,
|
|
});
|
|
trackAndPollTaskMock.mockResolvedValue({
|
|
status: "error",
|
|
error: 'Task ended in state "failed".',
|
|
});
|
|
refreshRegistryCredentialMock.mockResolvedValue({
|
|
status: "status",
|
|
credential: activeCredential,
|
|
});
|
|
|
|
// When
|
|
const outcome = await executeRegistryCredentialValidation("replacement");
|
|
|
|
// Then
|
|
expect(outcome).toEqual({ status: "replacement_failed" });
|
|
});
|
|
|
|
it("reports a validation still pending after the watch settles", async () => {
|
|
// Given
|
|
trackAndPollTaskMock.mockResolvedValue({
|
|
status: "error",
|
|
error: "The task expired before it could be tracked to completion.",
|
|
});
|
|
refreshRegistryCredentialMock.mockResolvedValue({
|
|
status: "status",
|
|
credential: pendingCredential,
|
|
});
|
|
|
|
// When
|
|
const outcome = await executeRegistryCredentialValidation("slow-key");
|
|
|
|
// Then
|
|
expect(outcome).toEqual({
|
|
status: "pending",
|
|
credential: pendingCredential,
|
|
});
|
|
});
|
|
|
|
it("passes submit failures through without watching any task", async () => {
|
|
// Given
|
|
submitRegistryCredentialMock
|
|
.mockResolvedValueOnce({ status: "access_denied" })
|
|
.mockResolvedValueOnce({
|
|
status: "replacement_failed",
|
|
credential: activeCredential,
|
|
})
|
|
.mockResolvedValueOnce({ status: "error" });
|
|
|
|
// When
|
|
const denied = await executeRegistryCredentialValidation("key");
|
|
const replacementFailed = await executeRegistryCredentialValidation("key");
|
|
const failed = await executeRegistryCredentialValidation("key");
|
|
|
|
// Then
|
|
expect(denied).toEqual({ status: "access_denied" });
|
|
expect(replacementFailed).toEqual({ status: "replacement_failed" });
|
|
expect(failed).toEqual({ status: "error" });
|
|
expect(trackAndPollTaskMock).not.toHaveBeenCalled();
|
|
expect(refreshRegistryCredentialMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("confirms and publishes once when validation finishes after the dialog deadline", async () => {
|
|
vi.useFakeTimers();
|
|
const changed = vi.fn();
|
|
window.addEventListener("registry-credential-changed", changed);
|
|
try {
|
|
let finish: (result: unknown) => void = () => {};
|
|
trackAndPollTaskMock.mockReturnValue(
|
|
new Promise((resolve) => {
|
|
finish = resolve;
|
|
}),
|
|
);
|
|
const waiting = executeRegistryCredentialValidation("slow-key");
|
|
await vi.advanceTimersByTimeAsync(30_000);
|
|
await expect(waiting).resolves.toEqual({ status: "pending" });
|
|
expect(refreshRegistryCredentialMock).not.toHaveBeenCalled();
|
|
finish({ status: "ready", result: { stored: true, error: null } });
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
expect(refreshRegistryCredentialMock).toHaveBeenCalledTimes(1);
|
|
expect(refreshRegistryCollectionsMock).toHaveBeenCalledTimes(1);
|
|
expect(changed).toHaveBeenCalledTimes(1);
|
|
expect(changed.mock.calls[0][0].detail.status).toBe("connected");
|
|
} finally {
|
|
window.removeEventListener("registry-credential-changed", changed);
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("publishes the same failure returned to the dialog when collections fail", async () => {
|
|
const changed = vi.fn();
|
|
window.addEventListener("registry-credential-changed", changed);
|
|
try {
|
|
refreshRegistryCollectionsMock.mockResolvedValue({
|
|
status: "unavailable",
|
|
});
|
|
const result = await executeRegistryCredentialValidation("key");
|
|
expect(result).toEqual({
|
|
status: "error",
|
|
message: "Registry collections could not be loaded. Try again.",
|
|
});
|
|
expect(changed).toHaveBeenCalledTimes(1);
|
|
expect(changed.mock.calls[0][0].detail).toBe(result);
|
|
} finally {
|
|
window.removeEventListener("registry-credential-changed", changed);
|
|
}
|
|
});
|
|
|
|
it.each([false, true])(
|
|
"does not confirm an unsettled task (priorConfigured: %s)",
|
|
async (priorConfigured) => {
|
|
submitRegistryCredentialMock.mockResolvedValue({
|
|
status: "submitted",
|
|
taskId: "task-1",
|
|
priorConfigured,
|
|
});
|
|
trackAndPollTaskMock.mockResolvedValue({ status: "pending" });
|
|
expect(await executeRegistryCredentialValidation("race-key")).toEqual({
|
|
status: "pending",
|
|
});
|
|
expect(refreshRegistryCredentialMock).not.toHaveBeenCalled();
|
|
expect(refreshRegistryCollectionsMock).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
|
|
it("fails safely when the submit RPC rejects", async () => {
|
|
// Given
|
|
submitRegistryCredentialMock.mockRejectedValue(new Error("rpc dropped"));
|
|
|
|
// When
|
|
const outcome = await executeRegistryCredentialValidation("key");
|
|
|
|
// Then
|
|
expect(outcome).toEqual({ status: "error" });
|
|
expect(trackAndPollTaskMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fails safely when the authoritative re-read RPC rejects", async () => {
|
|
// Given
|
|
refreshRegistryCredentialMock.mockRejectedValue(new Error("rpc dropped"));
|
|
|
|
// When
|
|
const outcome = await executeRegistryCredentialValidation("key");
|
|
|
|
// Then
|
|
expect(outcome).toEqual({ status: "error" });
|
|
});
|
|
|
|
it("fails safely when tracking throws before the authoritative re-read", async () => {
|
|
// Given
|
|
trackAndPollTaskMock.mockRejectedValue(new Error("watcher crashed"));
|
|
|
|
// When
|
|
const outcome = await executeRegistryCredentialValidation("key");
|
|
|
|
// Then
|
|
expect(outcome).toEqual({ status: "error" });
|
|
expect(refreshRegistryCredentialMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("propagates authoritative re-read failures after the watch", async () => {
|
|
// Given
|
|
refreshRegistryCredentialMock
|
|
.mockResolvedValueOnce({ status: "access_denied" })
|
|
.mockResolvedValueOnce({ status: "error" });
|
|
|
|
// When
|
|
const denied = await executeRegistryCredentialValidation("key");
|
|
const failed = await executeRegistryCredentialValidation("key");
|
|
|
|
// Then
|
|
expect(denied).toEqual({ status: "access_denied" });
|
|
expect(failed).toEqual({ status: "error" });
|
|
});
|
|
|
|
it("does not report a rejected replacement as connected when the prior key remains active", async () => {
|
|
submitRegistryCredentialMock.mockResolvedValue({
|
|
status: "submitted",
|
|
taskId: "task",
|
|
priorConfigured: true,
|
|
});
|
|
trackAndPollTaskMock.mockResolvedValue({
|
|
status: "ready",
|
|
result: { stored: false, error: "Invalid key" },
|
|
});
|
|
expect(await executeRegistryCredentialValidation("replacement")).toEqual({
|
|
status: "replacement_failed",
|
|
});
|
|
});
|
|
});
|