| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- import { beforeEach, describe, expect, it, vi } from "vitest";
- const getSessionMock = vi.fn();
- const findAllProvidersFreshMock = vi.fn();
- const findProviderVendorByIdMock = vi.fn();
- const getOrCreateProviderVendorIdFromUrlsMock = vi.fn();
- const computeVendorKeyMock = vi.fn();
- const backfillProviderEndpointsFromProvidersMock = vi.fn();
- const tryDeleteProviderVendorIfEmptyMock = vi.fn();
- const publishProviderCacheInvalidationMock = vi.fn();
- const dbMock = {
- transaction: vi.fn(),
- update: vi.fn(),
- };
- vi.mock("@/lib/auth", () => ({
- getSession: getSessionMock,
- }));
- vi.mock("@/repository/provider", () => ({
- findAllProviders: vi.fn(async () => []),
- findAllProvidersFresh: findAllProvidersFreshMock,
- findProviderById: vi.fn(async () => null),
- }));
- vi.mock("@/repository/provider-endpoints", () => ({
- computeVendorKey: computeVendorKeyMock,
- findProviderVendorById: findProviderVendorByIdMock,
- findProviderVendorsByIds: vi.fn(async (vendorIds: number[]) => {
- const vendors = await Promise.all(vendorIds.map((id) => findProviderVendorByIdMock(id)));
- return vendors.filter((vendor): vendor is NonNullable<typeof vendor> => vendor !== null);
- }),
- getOrCreateProviderVendorIdFromUrls: getOrCreateProviderVendorIdFromUrlsMock,
- backfillProviderEndpointsFromProviders: backfillProviderEndpointsFromProvidersMock,
- tryDeleteProviderVendorIfEmpty: tryDeleteProviderVendorIfEmptyMock,
- }));
- vi.mock("@/lib/cache/provider-cache", () => ({
- publishProviderCacheInvalidation: publishProviderCacheInvalidationMock,
- }));
- vi.mock("@/drizzle/db", () => ({
- db: dbMock,
- }));
- vi.mock("@/lib/logger", () => ({
- logger: {
- trace: vi.fn(),
- debug: vi.fn(),
- info: vi.fn(),
- warn: vi.fn(),
- error: vi.fn(),
- },
- }));
- describe("reclusterProviderVendors", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
- describe("permission checks", () => {
- it("returns error when not logged in", async () => {
- getSessionMock.mockResolvedValue(null);
- const { reclusterProviderVendors } = await import("@/actions/providers");
- const result = await reclusterProviderVendors({ confirm: false });
- expect(result.ok).toBe(false);
- expect(result.error).toBeDefined();
- });
- it("returns error when user is not admin", async () => {
- getSessionMock.mockResolvedValue({ user: { id: 1, role: "user" } });
- const { reclusterProviderVendors } = await import("@/actions/providers");
- const result = await reclusterProviderVendors({ confirm: false });
- expect(result.ok).toBe(false);
- expect(result.error).toBeDefined();
- });
- it("allows admin users", async () => {
- getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
- findAllProvidersFreshMock.mockResolvedValue([]);
- const { reclusterProviderVendors } = await import("@/actions/providers");
- const result = await reclusterProviderVendors({ confirm: false });
- expect(result.ok).toBe(true);
- });
- });
- describe("preview mode (confirm=false)", () => {
- it("returns empty changes when no providers", async () => {
- getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
- findAllProvidersFreshMock.mockResolvedValue([]);
- const { reclusterProviderVendors } = await import("@/actions/providers");
- const result = await reclusterProviderVendors({ confirm: false });
- expect(result.ok).toBe(true);
- if (result.ok) {
- expect(result.data.applied).toBe(false);
- expect(result.data.changes).toEqual([]);
- expect(result.data.preview.providersMoved).toBe(0);
- }
- });
- it("detects providers that need vendor change", async () => {
- getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
- findAllProvidersFreshMock.mockResolvedValue([
- {
- id: 1,
- name: "Provider 1",
- url: "http://192.168.1.1:8080/v1/messages",
- websiteUrl: null,
- providerVendorId: 1,
- },
- {
- id: 2,
- name: "Provider 2",
- url: "http://192.168.1.1:9090/v1/messages",
- websiteUrl: null,
- providerVendorId: 1, // Same vendor but different port - should change
- },
- ]);
- // Current vendor has domain "192.168.1.1" (old behavior)
- findProviderVendorByIdMock.mockResolvedValue({
- id: 1,
- websiteDomain: "192.168.1.1",
- });
- // New vendor keys include port
- computeVendorKeyMock
- .mockReturnValueOnce("192.168.1.1:8080")
- .mockReturnValueOnce("192.168.1.1:9090");
- const { reclusterProviderVendors } = await import("@/actions/providers");
- const result = await reclusterProviderVendors({ confirm: false });
- expect(result.ok).toBe(true);
- if (result.ok) {
- expect(result.data.applied).toBe(false);
- expect(result.data.preview.providersMoved).toBe(2);
- expect(result.data.changes.length).toBe(2);
- }
- });
- it("does not modify database in preview mode", async () => {
- getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
- findAllProvidersFreshMock.mockResolvedValue([
- {
- id: 1,
- name: "Provider 1",
- url: "http://192.168.1.1:8080/v1/messages",
- websiteUrl: null,
- providerVendorId: 1,
- },
- ]);
- findProviderVendorByIdMock.mockResolvedValue({
- id: 1,
- websiteDomain: "192.168.1.1",
- });
- computeVendorKeyMock.mockReturnValue("192.168.1.1:8080");
- const { reclusterProviderVendors } = await import("@/actions/providers");
- await reclusterProviderVendors({ confirm: false });
- expect(dbMock.transaction).not.toHaveBeenCalled();
- expect(publishProviderCacheInvalidationMock).not.toHaveBeenCalled();
- });
- it("skips providers with invalid URLs", async () => {
- getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
- findAllProvidersFreshMock.mockResolvedValue([
- {
- id: 1,
- name: "Invalid Provider",
- url: "://invalid",
- websiteUrl: null,
- providerVendorId: null,
- },
- ]);
- computeVendorKeyMock.mockReturnValue(null);
- const { reclusterProviderVendors } = await import("@/actions/providers");
- const result = await reclusterProviderVendors({ confirm: false });
- expect(result.ok).toBe(true);
- if (result.ok) {
- expect(result.data.preview.skippedInvalidUrl).toBe(1);
- expect(result.data.preview.providersMoved).toBe(0);
- }
- });
- });
- describe("apply mode (confirm=true)", () => {
- it("executes database updates in transaction", async () => {
- getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
- findAllProvidersFreshMock.mockResolvedValue([
- {
- id: 1,
- name: "Provider 1",
- url: "http://192.168.1.1:8080/v1/messages",
- websiteUrl: null,
- providerVendorId: 1,
- },
- ]);
- findProviderVendorByIdMock.mockResolvedValue({
- id: 1,
- websiteDomain: "192.168.1.1",
- });
- computeVendorKeyMock.mockReturnValue("192.168.1.1:8080");
- getOrCreateProviderVendorIdFromUrlsMock.mockResolvedValue(2);
- backfillProviderEndpointsFromProvidersMock.mockResolvedValue({});
- tryDeleteProviderVendorIfEmptyMock.mockResolvedValue(true);
- const tx = {
- update: vi.fn().mockReturnValue({
- set: vi.fn().mockReturnValue({
- where: vi.fn().mockResolvedValue({}),
- }),
- }),
- };
- dbMock.transaction.mockImplementation(async (fn) => {
- return fn(tx);
- });
- const { reclusterProviderVendors } = await import("@/actions/providers");
- const result = await reclusterProviderVendors({ confirm: true });
- expect(result.ok).toBe(true);
- if (result.ok) {
- expect(result.data.applied).toBe(true);
- }
- expect(dbMock.transaction).toHaveBeenCalled();
- expect(getOrCreateProviderVendorIdFromUrlsMock).toHaveBeenCalledWith(
- expect.objectContaining({
- providerUrl: "http://192.168.1.1:8080/v1/messages",
- websiteUrl: null,
- }),
- { tx }
- );
- });
- it("publishes cache invalidation after apply", async () => {
- getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
- findAllProvidersFreshMock.mockResolvedValue([
- {
- id: 1,
- name: "Provider 1",
- url: "http://192.168.1.1:8080/v1/messages",
- websiteUrl: null,
- providerVendorId: 1,
- },
- ]);
- findProviderVendorByIdMock.mockResolvedValue({
- id: 1,
- websiteDomain: "192.168.1.1",
- });
- computeVendorKeyMock.mockReturnValue("192.168.1.1:8080");
- getOrCreateProviderVendorIdFromUrlsMock.mockResolvedValue(2);
- backfillProviderEndpointsFromProvidersMock.mockResolvedValue({});
- tryDeleteProviderVendorIfEmptyMock.mockResolvedValue(true);
- dbMock.transaction.mockImplementation(async (fn) => {
- return fn({
- update: vi.fn().mockReturnValue({
- set: vi.fn().mockReturnValue({
- where: vi.fn().mockResolvedValue({}),
- }),
- }),
- });
- });
- const { reclusterProviderVendors } = await import("@/actions/providers");
- await reclusterProviderVendors({ confirm: true });
- expect(publishProviderCacheInvalidationMock).toHaveBeenCalled();
- });
- it("does not apply when no changes needed", async () => {
- getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
- findAllProvidersFreshMock.mockResolvedValue([
- {
- id: 1,
- name: "Provider 1",
- url: "http://192.168.1.1:8080/v1/messages",
- websiteUrl: null,
- providerVendorId: 1,
- },
- ]);
- // Vendor already has correct domain
- findProviderVendorByIdMock.mockResolvedValue({
- id: 1,
- websiteDomain: "192.168.1.1:8080",
- });
- computeVendorKeyMock.mockReturnValue("192.168.1.1:8080");
- const { reclusterProviderVendors } = await import("@/actions/providers");
- const result = await reclusterProviderVendors({ confirm: true });
- expect(result.ok).toBe(true);
- if (result.ok) {
- expect(result.data.applied).toBe(true);
- expect(result.data.preview.providersMoved).toBe(0);
- }
- expect(dbMock.transaction).not.toHaveBeenCalled();
- });
- });
- });
|