| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492 |
- import { beforeEach, describe, expect, it, vi } from "vitest";
- const getSessionMock = vi.fn();
- const updateProviderVendorMock = vi.fn();
- const deleteProviderVendorMock = vi.fn();
- const publishProviderCacheInvalidationMock = vi.fn();
- const findProviderEndpointByIdMock = vi.fn();
- const softDeleteProviderEndpointMock = vi.fn();
- const tryDeleteProviderVendorIfEmptyMock = vi.fn();
- const updateProviderEndpointMock = vi.fn();
- const findProviderEndpointProbeLogsBatchMock = vi.fn();
- const findVendorTypeEndpointStatsBatchMock = vi.fn();
- const hasEnabledProviderReferenceForVendorTypeUrlMock = vi.fn();
- const findDashboardProviderEndpointsByVendorAndTypeMock = vi.fn();
- vi.mock("@/lib/auth", () => ({
- getSession: getSessionMock,
- }));
- vi.mock("@/lib/cache/provider-cache", () => ({
- publishProviderCacheInvalidation: publishProviderCacheInvalidationMock,
- }));
- vi.mock("@/lib/logger", () => ({
- logger: {
- trace: vi.fn(),
- debug: vi.fn(),
- info: vi.fn(),
- warn: vi.fn(),
- error: vi.fn(),
- },
- }));
- vi.mock("@/lib/endpoint-circuit-breaker", () => ({
- getAllEndpointHealthStatusAsync: vi.fn(async () => ({})),
- getEndpointHealthInfo: vi.fn(async () => ({ health: {}, config: {} })),
- resetEndpointCircuit: vi.fn(async () => {}),
- }));
- vi.mock("@/lib/vendor-type-circuit-breaker", () => ({
- getVendorTypeCircuitInfo: vi.fn(async () => ({
- vendorId: 1,
- providerType: "claude",
- circuitState: "closed",
- circuitOpenUntil: null,
- lastFailureTime: null,
- manualOpen: false,
- })),
- resetVendorTypeCircuit: vi.fn(async () => {}),
- setVendorTypeCircuitManualOpen: vi.fn(async () => {}),
- }));
- vi.mock("@/lib/provider-endpoints/probe", () => ({
- probeProviderEndpointAndRecordByEndpoint: vi.fn(async () => null),
- }));
- vi.mock("@/repository/provider-endpoints-batch", () => ({
- findProviderEndpointProbeLogsBatch: findProviderEndpointProbeLogsBatchMock,
- findVendorTypeEndpointStatsBatch: findVendorTypeEndpointStatsBatchMock,
- }));
- vi.mock("@/repository/provider-endpoints", () => ({
- findDashboardProviderEndpointsByVendorAndType: findDashboardProviderEndpointsByVendorAndTypeMock,
- findEnabledProviderVendorTypePairs: vi.fn(async () => []),
- hasEnabledProviderReferenceForVendorTypeUrl: hasEnabledProviderReferenceForVendorTypeUrlMock,
- }));
- vi.mock("@/repository", () => ({
- createProviderEndpoint: vi.fn(async () => ({})),
- deleteProviderVendor: deleteProviderVendorMock,
- findProviderEndpointById: findProviderEndpointByIdMock,
- findProviderEndpointProbeLogs: vi.fn(async () => []),
- findProviderEndpointsByVendorAndType: vi.fn(async () => []),
- findProviderVendorById: vi.fn(async () => null),
- findProviderVendors: vi.fn(async () => []),
- softDeleteProviderEndpoint: softDeleteProviderEndpointMock,
- tryDeleteProviderVendorIfEmpty: tryDeleteProviderVendorIfEmptyMock,
- updateProviderEndpoint: updateProviderEndpointMock,
- updateProviderVendor: updateProviderVendorMock,
- }));
- describe("provider-endpoints actions", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- hasEnabledProviderReferenceForVendorTypeUrlMock.mockResolvedValue(false);
- findDashboardProviderEndpointsByVendorAndTypeMock.mockResolvedValue([]);
- });
- it("editProviderVendor: requires admin", async () => {
- getSessionMock.mockResolvedValue({ user: { role: "user" } });
- const { editProviderVendor } = await import("@/actions/provider-endpoints");
- const res = await editProviderVendor({ vendorId: 1, displayName: "x" });
- expect(res.ok).toBe(false);
- expect(res.errorCode).toBe("PERMISSION_DENIED");
- });
- it("getDashboardProviderEndpoints: requires admin", async () => {
- getSessionMock.mockResolvedValue({ user: { role: "user" } });
- const { getDashboardProviderEndpoints } = await import("@/actions/provider-endpoints");
- const res = await getDashboardProviderEndpoints({ vendorId: 1, providerType: "claude" });
- expect(res).toEqual([]);
- expect(findDashboardProviderEndpointsByVendorAndTypeMock).not.toHaveBeenCalled();
- });
- it("getDashboardProviderEndpoints: invalid input returns empty list", async () => {
- getSessionMock.mockResolvedValue({ user: { role: "admin" } });
- const { getDashboardProviderEndpoints } = await import("@/actions/provider-endpoints");
- const res = await getDashboardProviderEndpoints({ vendorId: 0, providerType: "claude" });
- expect(res).toEqual([]);
- expect(findDashboardProviderEndpointsByVendorAndTypeMock).not.toHaveBeenCalled();
- });
- it("getDashboardProviderEndpoints: returns endpoints in use for enabled providers", async () => {
- getSessionMock.mockResolvedValue({ user: { role: "admin" } });
- const endpoints = [
- {
- id: 1,
- vendorId: 10,
- providerType: "claude",
- url: "https://api.example.com",
- label: null,
- sortOrder: 0,
- isEnabled: true,
- lastProbedAt: null,
- lastProbeOk: null,
- lastProbeStatusCode: null,
- lastProbeLatencyMs: null,
- lastProbeErrorType: null,
- lastProbeErrorMessage: null,
- createdAt: new Date(),
- updatedAt: new Date(),
- deletedAt: null,
- },
- ];
- findDashboardProviderEndpointsByVendorAndTypeMock.mockResolvedValue(endpoints);
- const { getDashboardProviderEndpoints } = await import("@/actions/provider-endpoints");
- const res = await getDashboardProviderEndpoints({ vendorId: 10, providerType: "claude" });
- expect(res).toEqual(endpoints);
- expect(findDashboardProviderEndpointsByVendorAndTypeMock).toHaveBeenCalledWith(10, "claude");
- });
- it("editProviderVendor: computes favicon", async () => {
- getSessionMock.mockResolvedValue({ user: { role: "admin" } });
- updateProviderVendorMock.mockResolvedValue({
- id: 1,
- websiteDomain: "example.com",
- displayName: "Example",
- websiteUrl: "https://example.com/path",
- faviconUrl: "https://www.google.com/s2/favicons?domain=example.com&sz=32",
- createdAt: new Date(),
- updatedAt: new Date(),
- });
- const { editProviderVendor } = await import("@/actions/provider-endpoints");
- const res = await editProviderVendor({
- vendorId: 1,
- displayName: "Example",
- websiteUrl: "https://example.com/path",
- });
- expect(res.ok).toBe(true);
- expect(updateProviderVendorMock).toHaveBeenCalledWith(
- 1,
- expect.objectContaining({
- displayName: "Example",
- websiteUrl: "https://example.com/path",
- faviconUrl: "https://www.google.com/s2/favicons?domain=example.com&sz=32",
- })
- );
- });
- it("editProviderVendor: clearing websiteUrl clears faviconUrl", async () => {
- getSessionMock.mockResolvedValue({ user: { role: "admin" } });
- updateProviderVendorMock.mockResolvedValue({
- id: 1,
- websiteDomain: "example.com",
- displayName: null,
- websiteUrl: null,
- faviconUrl: null,
- createdAt: new Date(),
- updatedAt: new Date(),
- });
- const { editProviderVendor } = await import("@/actions/provider-endpoints");
- const res = await editProviderVendor({
- vendorId: 1,
- websiteUrl: null,
- });
- expect(res.ok).toBe(true);
- expect(updateProviderVendorMock).toHaveBeenCalledWith(
- 1,
- expect.objectContaining({
- websiteUrl: null,
- faviconUrl: null,
- })
- );
- });
- it("editProviderEndpoint: conflict maps to CONFLICT errorCode", async () => {
- getSessionMock.mockResolvedValue({ user: { role: "admin" } });
- findProviderEndpointByIdMock.mockResolvedValue({
- id: 42,
- vendorId: 123,
- providerType: "claude",
- url: "https://api.example.com",
- label: null,
- sortOrder: 0,
- isEnabled: true,
- lastProbedAt: null,
- lastProbeOk: null,
- lastProbeStatusCode: null,
- lastProbeLatencyMs: null,
- lastProbeErrorType: null,
- lastProbeErrorMessage: null,
- createdAt: new Date(),
- updatedAt: new Date(),
- deletedAt: null,
- });
- updateProviderEndpointMock.mockRejectedValue(
- Object.assign(new Error("[ProviderEndpointEdit] endpoint conflict"), {
- code: "PROVIDER_ENDPOINT_CONFLICT",
- })
- );
- const { editProviderEndpoint } = await import("@/actions/provider-endpoints");
- const res = await editProviderEndpoint({
- endpointId: 42,
- url: "https://next.example.com/v1/messages",
- });
- expect(res.ok).toBe(false);
- expect(res.errorCode).toBe("CONFLICT");
- expect(res.error).not.toContain("duplicate key value");
- });
- it("editProviderEndpoint: success returns ok with endpoint payload", async () => {
- getSessionMock.mockResolvedValue({ user: { role: "admin" } });
- const endpoint = {
- id: 42,
- vendorId: 123,
- providerType: "claude" as const,
- url: "https://next.example.com/v1/messages",
- label: "primary",
- sortOrder: 7,
- isEnabled: false,
- lastProbedAt: null,
- lastProbeOk: null,
- lastProbeStatusCode: null,
- lastProbeLatencyMs: null,
- lastProbeErrorType: null,
- lastProbeErrorMessage: null,
- createdAt: new Date("2026-01-01T00:00:00.000Z"),
- updatedAt: new Date("2026-01-01T00:00:00.000Z"),
- deletedAt: null,
- };
- findProviderEndpointByIdMock.mockResolvedValue(endpoint);
- updateProviderEndpointMock.mockResolvedValue(endpoint);
- const { editProviderEndpoint } = await import("@/actions/provider-endpoints");
- const res = await editProviderEndpoint({
- endpointId: 42,
- url: endpoint.url,
- label: endpoint.label,
- sortOrder: endpoint.sortOrder,
- isEnabled: endpoint.isEnabled,
- });
- expect(res.ok).toBe(true);
- expect(res.data?.endpoint).toEqual(endpoint);
- expect(updateProviderEndpointMock).toHaveBeenCalledWith(42, {
- url: endpoint.url,
- label: endpoint.label,
- sortOrder: endpoint.sortOrder,
- isEnabled: endpoint.isEnabled,
- });
- });
- it("removeProviderVendor: deletes vendor and publishes cache invalidation", async () => {
- getSessionMock.mockResolvedValue({ user: { role: "admin" } });
- deleteProviderVendorMock.mockResolvedValue(true);
- publishProviderCacheInvalidationMock.mockResolvedValue(undefined);
- const { removeProviderVendor } = await import("@/actions/provider-endpoints");
- const res = await removeProviderVendor({ vendorId: 1 });
- expect(res.ok).toBe(true);
- expect(deleteProviderVendorMock).toHaveBeenCalledWith(1);
- expect(publishProviderCacheInvalidationMock).toHaveBeenCalledTimes(1);
- });
- it("removeProviderVendor: still ok when cache invalidation fails", async () => {
- getSessionMock.mockResolvedValue({ user: { role: "admin" } });
- deleteProviderVendorMock.mockResolvedValue(true);
- publishProviderCacheInvalidationMock.mockRejectedValue(new Error("boom"));
- const { removeProviderVendor } = await import("@/actions/provider-endpoints");
- const res = await removeProviderVendor({ vendorId: 1 });
- expect(res.ok).toBe(true);
- expect(deleteProviderVendorMock).toHaveBeenCalledWith(1);
- });
- it("removeProviderEndpoint: triggers vendor cleanup after soft delete", async () => {
- getSessionMock.mockResolvedValue({ user: { role: "admin" } });
- findProviderEndpointByIdMock.mockResolvedValue({
- id: 99,
- vendorId: 123,
- providerType: "claude",
- url: "https://api.example.com",
- label: null,
- sortOrder: 0,
- isEnabled: true,
- lastProbedAt: null,
- lastProbeOk: null,
- lastProbeStatusCode: null,
- lastProbeLatencyMs: null,
- lastProbeErrorType: null,
- lastProbeErrorMessage: null,
- createdAt: new Date(),
- updatedAt: new Date(),
- deletedAt: null,
- });
- softDeleteProviderEndpointMock.mockResolvedValue(true);
- tryDeleteProviderVendorIfEmptyMock.mockResolvedValue(true);
- hasEnabledProviderReferenceForVendorTypeUrlMock.mockResolvedValue(false);
- const { removeProviderEndpoint } = await import("@/actions/provider-endpoints");
- const res = await removeProviderEndpoint({ endpointId: 99 });
- expect(res.ok).toBe(true);
- const { resetEndpointCircuit } = await import("@/lib/endpoint-circuit-breaker");
- expect(resetEndpointCircuit).toHaveBeenCalledWith(99);
- expect(tryDeleteProviderVendorIfEmptyMock).toHaveBeenCalledWith(123);
- });
- it("probeProviderEndpoint: calls probeProviderEndpointAndRecordByEndpoint and returns result", async () => {
- getSessionMock.mockResolvedValue({ user: { role: "admin" } });
- const endpoint = {
- id: 7,
- vendorId: 123,
- providerType: "claude",
- url: "https://api.example.com",
- label: null,
- sortOrder: 0,
- isEnabled: true,
- lastProbedAt: null,
- lastProbeOk: null,
- lastProbeStatusCode: null,
- lastProbeLatencyMs: null,
- lastProbeErrorType: null,
- lastProbeErrorMessage: null,
- createdAt: new Date(),
- updatedAt: new Date(),
- deletedAt: null,
- };
- findProviderEndpointByIdMock.mockResolvedValue(endpoint);
- const { probeProviderEndpointAndRecordByEndpoint } = await import(
- "@/lib/provider-endpoints/probe"
- );
- const result = {
- ok: true,
- method: "HEAD",
- statusCode: 200,
- latencyMs: 10,
- errorType: null,
- errorMessage: null,
- } as const;
- vi.mocked(probeProviderEndpointAndRecordByEndpoint).mockResolvedValue(result);
- const { probeProviderEndpoint } = await import("@/actions/provider-endpoints");
- const res = await probeProviderEndpoint({ endpointId: 7, timeoutMs: 5000 });
- expect(res.ok).toBe(true);
- expect(probeProviderEndpointAndRecordByEndpoint).toHaveBeenCalledWith({
- endpoint,
- source: "manual",
- timeoutMs: 5000,
- });
- expect(res.data?.result).toEqual(result);
- });
- describe("batchGetEndpointCircuitInfo", () => {
- it("returns circuit info for multiple endpoints", async () => {
- getSessionMock.mockResolvedValue({ user: { role: "admin" } });
- const { getAllEndpointHealthStatusAsync } = await import("@/lib/endpoint-circuit-breaker");
- vi.mocked(getAllEndpointHealthStatusAsync).mockResolvedValue({
- 1: {
- failureCount: 0,
- lastFailureTime: null,
- circuitState: "closed",
- circuitOpenUntil: null,
- halfOpenSuccessCount: 0,
- },
- 2: {
- failureCount: 5,
- lastFailureTime: Date.now(),
- circuitState: "open",
- circuitOpenUntil: Date.now() + 60000,
- halfOpenSuccessCount: 0,
- },
- 3: {
- failureCount: 1,
- lastFailureTime: Date.now() - 1000,
- circuitState: "half-open",
- circuitOpenUntil: null,
- halfOpenSuccessCount: 0,
- },
- });
- const { batchGetEndpointCircuitInfo } = await import("@/actions/provider-endpoints");
- const res = await batchGetEndpointCircuitInfo({ endpointIds: [1, 2, 3] });
- expect(res.ok).toBe(true);
- expect(getAllEndpointHealthStatusAsync).toHaveBeenCalledWith([1, 2, 3]);
- expect(res.data).toHaveLength(3);
- expect(res.data?.[0]).toEqual({
- endpointId: 1,
- circuitState: "closed",
- failureCount: 0,
- circuitOpenUntil: null,
- });
- expect(res.data?.[1]).toEqual({
- endpointId: 2,
- circuitState: "open",
- failureCount: 5,
- circuitOpenUntil: expect.any(Number),
- });
- expect(res.data?.[2]).toEqual({
- endpointId: 3,
- circuitState: "half-open",
- failureCount: 1,
- circuitOpenUntil: null,
- });
- });
- it("returns empty array for empty input", async () => {
- getSessionMock.mockResolvedValue({ user: { role: "admin" } });
- const { batchGetEndpointCircuitInfo } = await import("@/actions/provider-endpoints");
- const res = await batchGetEndpointCircuitInfo({ endpointIds: [] });
- expect(res.ok).toBe(true);
- expect(res.data).toEqual([]);
- });
- it("requires admin session", async () => {
- getSessionMock.mockResolvedValue({ user: { role: "user" } });
- const { batchGetEndpointCircuitInfo } = await import("@/actions/provider-endpoints");
- const res = await batchGetEndpointCircuitInfo({ endpointIds: [1, 2] });
- expect(res.ok).toBe(false);
- expect(res.errorCode).toBe("PERMISSION_DENIED");
- });
- it("validates endpointIds are positive integers", async () => {
- getSessionMock.mockResolvedValue({ user: { role: "admin" } });
- const { batchGetEndpointCircuitInfo } = await import("@/actions/provider-endpoints");
- const res = await batchGetEndpointCircuitInfo({ endpointIds: [0, -1, 1] });
- expect(res.ok).toBe(false);
- expect(res.errorCode).toBe("MIN_VALUE");
- });
- });
- });
|