import { BackendHandler } from "../backend-handler"; import type { Account, Environment, MarketplaceItem, MarketplaceService, Resource, Service, Tenant, UserData } from "@kumori/aurora-interfaces"; // ── Mocks de módulos externos ───────────────────────────────────────────────── jest.mock("../event-helper", () => jest.fn().mockImplementation((globalEventHandler: any) => globalEventHandler), ); jest.mock("../api/user-api-service", () => ({ getUserHTTP: jest.fn().mockResolvedValue({}), loadUser: jest.fn(), isUserLogged: jest.fn().mockResolvedValue(true), createUser: jest.fn(), updateUser: jest.fn(), deleteUSer: jest.fn(), })); jest.mock("../api/tenant-api-service", () => ({ createTenant: jest.fn(), createTenantHTTP: jest.fn(), updateTenant: jest.fn(), updateTenantHTTP: jest.fn(), deleteTenant: jest.fn(), createRegistry: jest.fn(), updateRegistry: jest.fn(), deleteRegistry: jest.fn(), inviteUser: jest.fn(), removeUser: jest.fn(), updateUserRole: jest.fn(), acceptInvite: jest.fn(), rejectInvite: jest.fn(), createToken: jest.fn(), deleteToken: jest.fn(), })); jest.mock("../api/account-api-service", () => ({ createAccount: jest.fn(), updateAccount: jest.fn(), deleteAccount: jest.fn(), clearAccount: jest.fn(), })); jest.mock("../api/environment-api-service", () => ({ createEnvironment: jest.fn(), updateEnvironment: jest.fn(), deleteEnvironment: jest.fn(), clearEnvironment: jest.fn(), scaleEnvironment: jest.fn(), })); jest.mock("../api/service-api-service", () => ({ deployService: jest.fn(), updateService: jest.fn(), deleteService: jest.fn(), restartService: jest.fn(), redeployService: jest.fn(), requestRevisionData: jest.fn(), updateServiceLinks: jest.fn(), changeRevision: jest.fn(), restartInstance: jest.fn(), })); jest.mock("../api/marketplace-api-service", () => ({ deployMarketplaceItem: jest.fn(), getMarketplaceItems: jest.fn().mockResolvedValue({ items: [] }), getMarketplaceSchema: jest.fn(), })); jest.mock("../api/resources-api-service", () => ({ createResource: jest.fn(), updateResource: jest.fn(), deleteResource: jest.fn(), })); jest.mock("../api/planProvider-api-service", () => ({ getPlanProviders: jest.fn(), })); jest.mock("../websocket-manager", () => ({ initializeGlobalWebSocketClient: jest.fn(), updateUserComplete: jest.fn(), fetchAndStoreMarketplaceSchema: jest.fn().mockResolvedValue(undefined), getReferenceDomain: jest.fn().mockReturnValue("test-domain.com"), })); // ── Helpers para construir el dummy handler ─────────────────────────────────── const makeFns = (...names: string[]) => Object.fromEntries(names.map((n) => [n, jest.fn()])); const createDummyHandler = () => ({ subscribe: jest.fn(), publish: jest.fn(), unsubscribe: jest.fn(), notification: { publish: makeFns("creation", "deletion", "read"), subscribe: makeFns("creation", "deletion", "read"), unsubscribe: makeFns("creation", "deletion", "read"), }, tenant: { publish: makeFns("creation", "created", "update", "updated", "delete", "deleted"), subscribe: makeFns( "creation", "created", "creationError", "update", "updated", "updateError", "delete", "deleted", "deletionError", "createRegistry", "registryCreated", "registryCreationError", "updateRegistry", "registryUpdated", "registryUpdateError", "deleteRegistry", "registryDeleted", "registryDeletionError", "inviteUser", "userInvited", "inviteError", "removeUser", "userRemoved", "removeUserError", "updateInvite", "inviteUpdated", "inviteUpdateError", "acceptInvite", "inviteAccepted", "acceptInviteError", "rejectInvite", "inviteRejected", "inviteRejectError", "createToken", "tokenCreated", "tokenCreationError", "deleteToken", "tokenDeleted", "tokenDeletionError", ), unsubscribe: makeFns( "creation", "created", "creationError", "update", "updated", "updateError", "delete", "deleted", "deletionError", "createRegistry", "registryCreated", "registryCreationError", "updateRegistry", "registryUpdated", "registryUpdateError", "deleteRegistry", "registryDeleted", "registryDeletionError", "inviteUser", "userInvited", "inviteError", "removeUser", "userRemoved", "removeUserError", "updateInvite", "inviteUpdated", "inviteUpdateError", "acceptInvite", "inviteAccepted", "acceptInviteError", "rejectInvite", "inviteRejected", "inviteRejectError", "createToken", "tokenCreated", "tokenCreationError", "deleteToken", "tokenDeleted", "tokenDeletionError", ), }, user: { publish: makeFns("creation", "load", "loaded", "update", "delete", "authError"), subscribe: makeFns( "creation", "created", "creationError", "update", "updated", "updateError", "load", "loaded", "loadError", "delete", "deleted", "deletionError", "authError", ), unsubscribe: makeFns( "creation", "created", "creationError", "update", "updated", "updateError", "load", "loaded", "loadError", "delete", "deleted", "deletionError", "authError", ), }, account: { publish: makeFns("creation", "created", "update", "delete"), subscribe: makeFns( "creation", "created", "creationError", "update", "updated", "updateError", "delete", "deleted", "deletionError", "clean", "cleaned", "cleanError", ), unsubscribe: makeFns( "creation", "created", "creationError", "update", "updated", "updateError", "delete", "deleted", "deletionError", "clean", "cleaned", "cleanError", ), }, plan: { publish: makeFns("upgrade", "upgraded", "downgrade", "downgraded"), subscribe: makeFns("upgrade", "upgraded", "upgradeError", "downgrade", "downgraded", "downgradeError"), unsubscribe: makeFns("upgrade", "upgraded", "upgradeError", "downgrade", "downgraded", "downgradeError"), }, environment: { publish: makeFns("creation", "created", "update", "delete", "clean", "scale"), subscribe: makeFns( "creation", "created", "creationError", "update", "updated", "updateError", "delete", "deleted", "deletionError", "clean", "cleaned", "cleanError", "scale", "scaled", "scaleError", ), unsubscribe: makeFns( "creation", "created", "creationError", "update", "updated", "updateError", "delete", "deleted", "deletionError", "clean", "cleaned", "cleanError", "scale", "scaled", "scaleError", ), }, service: { publish: makeFns("deploy", "deployed", "deploymentError", "update", "updated", "delete", "deleted", "restart"), subscribe: makeFns( "deploy", "deployed", "deploymentError", "update", "updated", "updateError", "delete", "deleted", "deletionError", "requestLogs", "restart", "restarted", "restartError", "requestRevisionData", "updateServiceLinks", "changeRevision", "revisionChanged", "revisionChangeError", "restartInstance", "instanceRestarted", "instanceRestartError", ), unsubscribe: makeFns( "deploy", "deployed", "deploymentError", "update", "updated", "updateError", "delete", "deleted", "deletionError", "requestLogs", "restart", "restarted", "restartError", "requestRevisionData", "updateServiceLinks", "changeRevision", "revisionChanged", "revisionChangeError", "restartInstance", "instanceRestarted", "instanceRestartError", ), }, marketplace: { publish: makeFns( "deployItem", "itemDeployed", "deploymentError", "updateItem", "itemUpdated", "updateError", "deleteItem", "itemDeleted", "deletionError", "loadItems", "itemsLoaded", "loadSchema", "schemaLoaded", "schemaLoadError", ), subscribe: makeFns( "deployItem", "itemDeployed", "deploymentError", "updateItem", "itemUpdated", "updateError", "deleteItem", "itemDeleted", "deletionError", "loadItems", "itemsLoaded", "loadSchema", "schemaLoaded", "schemaLoadError", ), unsubscribe: makeFns( "deployItem", "itemDeployed", "deploymentError", "updateItem", "itemUpdated", "updateError", "deleteItem", "itemDeleted", "deletionError", "loadItems", "itemsLoaded", "loadSchema", "schemaLoaded", "schemaLoadError", ), }, resource: { publish: makeFns("creation", "created", "update", "delete", "deleted"), subscribe: makeFns( "creation", "created", "creationError", "update", "updated", "updateError", "delete", "deleted", "deletionError", ), unsubscribe: makeFns( "creation", "created", "creationError", "update", "updated", "updateError", "delete", "deleted", "deletionError", ), }, organization: { publish: makeFns("creation", "update", "delete"), subscribe: makeFns( "creation", "created", "creationError", "update", "updated", "updateError", "delete", "deleted", "deletionError", ), unsubscribe: makeFns( "creation", "created", "creationError", "update", "updated", "updateError", "delete", "deleted", "deletionError", ), }, planProviders: { publish: makeFns("loadPlans", "plansLoaded", "loadError"), subscribe: makeFns("loadPlans", "plansLoaded", "loadError"), unsubscribe: makeFns("loadPlans", "plansLoaded", "loadError"), }, }); // ── Helper para construir y esperar que el constructor termine ──────────────── async function buildHandler(route = "home", handler?: ReturnType) { const h = handler ?? createDummyHandler(); const bh = new BackendHandler(route, h, "http://localhost:9080", "v1"); await Promise.resolve(); // flush microtask queue (getUserHTTP().then) return { bh, h }; } // ───────────────────────────────────────────────────────────────────────────── // Tests // ───────────────────────────────────────────────────────────────────────────── describe("BackendHandler - constructor validation", () => { it("lanza error si globalEventHandler es null", () => { expect(() => new BackendHandler("home", null, "http://localhost", "v1")).toThrow( "globalEventHandler inválido", ); }); it("lanza error si globalEventHandler no tiene subscribe", () => { expect(() => new BackendHandler("home", { publish: jest.fn() }, "http://localhost", "v1")).toThrow( "globalEventHandler inválido", ); }); it("no lanza error con handler válido", async () => { const { h } = await buildHandler(); expect(h.user.subscribe.load).toHaveBeenCalled(); }); }); describe("BackendHandler - constructor suscripciones iniciales", () => { it("suscribe a user.load y user.loaded en el constructor", async () => { const { h } = await buildHandler(); expect(h.user.subscribe.load).toHaveBeenCalled(); expect(h.user.subscribe.loaded).toHaveBeenCalled(); }); it("publica user.load tras inicializar", async () => { const { h } = await buildHandler(); expect(h.user.publish.load).toHaveBeenCalled(); }); it("invoca callback de user.load con initializeGlobalWebSocketClient", async () => { const { initializeGlobalWebSocketClient } = await import("../websocket-manager"); const { h } = await buildHandler(); const loadCb = (h.user.subscribe.load as jest.Mock).mock.calls[0][0]; loadCb({} as UserData); expect(initializeGlobalWebSocketClient).toHaveBeenCalled(); }); it("invoca callback de user.loaded con updateUserComplete", async () => { const { updateUserComplete } = await import("../websocket-manager"); const { h } = await buildHandler(); const loadedCb = (h.user.subscribe.loaded as jest.Mock).mock.calls[0][0]; loadedCb({ id: "u1" } as any); expect(updateUserComplete).toHaveBeenCalledWith({ id: "u1" }); }); it("llama a subscribeForRoute en el constructor", async () => { const { h } = await buildHandler("home"); expect(h.tenant.subscribe.creation).toHaveBeenCalled(); expect(h.user.subscribe.creation).toHaveBeenCalled(); expect(h.account.subscribe.creation).toHaveBeenCalled(); expect(h.environment.subscribe.creation).toHaveBeenCalled(); expect(h.service.subscribe.deploy).toHaveBeenCalled(); expect(h.marketplace.subscribe.deployItem).toHaveBeenCalled(); expect(h.resource.subscribe.creation).toHaveBeenCalled(); expect(h.organization.subscribe.creation).toHaveBeenCalled(); expect(h.plan.subscribe.upgrade).toHaveBeenCalled(); expect(h.planProviders.subscribe.loadPlans).toHaveBeenCalled(); }); }); describe("BackendHandler - isUserLoggedIn", () => { it("retorna true cuando isUserLogged devuelve true", async () => { const { isUserLogged } = await import("../api/user-api-service"); (isUserLogged as jest.Mock).mockResolvedValue(true); const { bh } = await buildHandler(); const result = await bh.isUserLoggedIn(); expect(result).toBe(true); }); it("retorna false cuando isUserLogged devuelve false", async () => { const { isUserLogged } = await import("../api/user-api-service"); (isUserLogged as jest.Mock).mockResolvedValue(false); const { bh } = await buildHandler(); const result = await bh.isUserLoggedIn(); expect(result).toBe(false); }); it("retorna false cuando isUserLogged devuelve string", async () => { const { isUserLogged } = await import("../api/user-api-service"); (isUserLogged as jest.Mock).mockResolvedValue("yes"); const { bh } = await buildHandler(); const result = await bh.isUserLoggedIn(); expect(result).toBe(false); }); }); describe("BackendHandler - changeRoute y unsubscribe", () => { it("changeRoute desuscribe eventos anteriores y suscribe nuevos", async () => { const { bh, h } = await buildHandler("home"); const firstCallCount = (h.tenant.subscribe.creation as jest.Mock).mock.calls.length; bh.changeRoute("tenants"); await Promise.resolve(); expect(h.tenant.unsubscribe.creation).toHaveBeenCalled(); expect((h.tenant.subscribe.creation as jest.Mock).mock.calls.length).toBeGreaterThan(firstCallCount); }); it("changeRoute múltiple acumula y limpia correctamente", async () => { const { bh, h } = await buildHandler("home"); bh.changeRoute("accounts"); bh.changeRoute("environments"); expect(h.account.unsubscribe.creation).toHaveBeenCalled(); }); }); // ── Callbacks de Tenant ─────────────────────────────────────────────────────── describe("BackendHandler - callbacks de Tenant", () => { it("creation callback llama a createTenant", async () => { const { createTenant } = await import("../api/tenant-api-service"); const { h } = await buildHandler(); const cb = (h.tenant.subscribe.creation as jest.Mock).mock.calls[0][0]; const tenant = { id: "t1", name: "T1" } as Tenant; cb(tenant); expect(createTenant).toHaveBeenCalledWith(tenant, ""); }); it("update callback llama a updateTenant", async () => { const { updateTenant } = await import("../api/tenant-api-service"); const { h } = await buildHandler(); const cb = (h.tenant.subscribe.update as jest.Mock).mock.calls[0][0]; const tenant = { id: "t1" } as Tenant; cb(tenant); expect(updateTenant).toHaveBeenCalledWith(tenant, ""); }); it("delete callback llama a deleteTenant", async () => { const { deleteTenant } = await import("../api/tenant-api-service"); const { h } = await buildHandler(); const cb = (h.tenant.subscribe.delete as jest.Mock).mock.calls[0][0]; cb({ id: "t1" } as Tenant); expect(deleteTenant).toHaveBeenCalled(); }); it("createRegistry callback llama a createRegistry", async () => { const { createRegistry } = await import("../api/tenant-api-service"); const { h } = await buildHandler(); const cb = (h.tenant.subscribe.createRegistry as jest.Mock).mock.calls[0][0]; cb({ tenant: { id: "t1" } as Tenant, registry: { name: "reg1" } as any }); expect(createRegistry).toHaveBeenCalled(); }); it("updateRegistry callback llama a updateRegistry", async () => { const { updateRegistry } = await import("../api/tenant-api-service"); const { h } = await buildHandler(); const cb = (h.tenant.subscribe.updateRegistry as jest.Mock).mock.calls[0][0]; cb({ tenant: { id: "t1" } as Tenant, registry: { name: "reg1" } as any }); expect(updateRegistry).toHaveBeenCalled(); }); it("deleteRegistry callback llama a deleteRegistry", async () => { const { deleteRegistry } = await import("../api/tenant-api-service"); const { h } = await buildHandler(); const cb = (h.tenant.subscribe.deleteRegistry as jest.Mock).mock.calls[0][0]; cb({ tenant: { id: "t1" } as Tenant, registry: { name: "reg1" } as any }); expect(deleteRegistry).toHaveBeenCalled(); }); it("inviteUser callback llama a inviteUser", async () => { const { inviteUser } = await import("../api/tenant-api-service"); const { h } = await buildHandler(); const cb = (h.tenant.subscribe.inviteUser as jest.Mock).mock.calls[0][0]; cb({ tenant: "t1", user: "u1", role: "admin" as any }); expect(inviteUser).toHaveBeenCalled(); }); it("userInvited callback llama a updateUserRole", async () => { const { updateUserRole } = await import("../api/tenant-api-service"); const { h } = await buildHandler(); const cb = (h.tenant.subscribe.userInvited as jest.Mock).mock.calls[0][0]; cb({ user: "u1", tenant: "t1", role: "member" as any }); expect(updateUserRole).toHaveBeenCalled(); }); it("removeUser callback llama a removeUser", async () => { const { removeUser } = await import("../api/tenant-api-service"); const { h } = await buildHandler(); const cb = (h.tenant.subscribe.removeUser as jest.Mock).mock.calls[0][0]; cb({ tenant: "t1", user: "u1" }); expect(removeUser).toHaveBeenCalled(); }); it("updateInvite callback llama a updateUserRole", async () => { const { updateUserRole } = await import("../api/tenant-api-service"); const { h } = await buildHandler(); const cb = (h.tenant.subscribe.updateInvite as jest.Mock).mock.calls[0][0]; cb({ user: "u1", tenant: "t1", role: "member" as any }); expect(updateUserRole).toHaveBeenCalled(); }); it("acceptInvite callback llama a acceptInvite", async () => { const { acceptInvite } = await import("../api/tenant-api-service"); const { h } = await buildHandler(); const cb = (h.tenant.subscribe.acceptInvite as jest.Mock).mock.calls[0][0]; cb({ tenant: "t1" }); expect(acceptInvite).toHaveBeenCalledWith("t1", ""); }); it("rejectInvite callback llama a rejectInvite", async () => { const { rejectInvite } = await import("../api/tenant-api-service"); const { h } = await buildHandler(); const cb = (h.tenant.subscribe.rejectInvite as jest.Mock).mock.calls[0][0]; cb({ tenant: "t1", leave: true }); expect(rejectInvite).toHaveBeenCalledWith("t1", "", true); }); it("createToken callback llama a createToken", async () => { const { createToken } = await import("../api/tenant-api-service"); const { h } = await buildHandler(); const cb = (h.tenant.subscribe.createToken as jest.Mock).mock.calls[0][0]; cb({ tenant: "t1", expiration: "2025-01-01", description: "desc" }); expect(createToken).toHaveBeenCalled(); }); it("deleteToken callback llama a deleteToken", async () => { const { deleteToken } = await import("../api/tenant-api-service"); const { h } = await buildHandler(); const cb = (h.tenant.subscribe.deleteToken as jest.Mock).mock.calls[0][0]; cb({ tenant: "t1", token: "tok123" }); expect(deleteToken).toHaveBeenCalledWith("t1", "tok123", ""); }); it("loadPlans callback llama a getPlanProviders", async () => { const { getPlanProviders } = await import("../api/planProvider-api-service"); const { h } = await buildHandler(); const cb = (h.planProviders.subscribe.loadPlans as jest.Mock).mock.calls[0][0]; cb(); expect(getPlanProviders).toHaveBeenCalled(); }); it("empty callbacks (created, creationError, etc.) no lanzan error", async () => { const { h } = await buildHandler(); const created = (h.tenant.subscribe.created as jest.Mock).mock.calls[0][0]; const creationError = (h.tenant.subscribe.creationError as jest.Mock).mock.calls[0][0]; const updated = (h.tenant.subscribe.updated as jest.Mock).mock.calls[0][0]; const updateError = (h.tenant.subscribe.updateError as jest.Mock).mock.calls[0][0]; const deleted = (h.tenant.subscribe.deleted as jest.Mock).mock.calls[0][0]; const deletionError = (h.tenant.subscribe.deletionError as jest.Mock).mock.calls[0][0]; expect(() => created({})).not.toThrow(); expect(() => creationError({})).not.toThrow(); expect(() => updated({})).not.toThrow(); expect(() => updateError({})).not.toThrow(); expect(() => deleted({})).not.toThrow(); expect(() => deletionError({})).not.toThrow(); }); }); // ── Callbacks de User ───────────────────────────────────────────────────────── describe("BackendHandler - callbacks de User", () => { it("creation callback llama a createUser", async () => { const { createUser } = await import("../api/user-api-service"); const { h } = await buildHandler(); const cb = (h.user.subscribe.creation as jest.Mock).mock.calls[0][0]; cb({} as UserData); expect(createUser).toHaveBeenCalled(); }); it("update callback llama a updateUser", async () => { const { updateUser } = await import("../api/user-api-service"); const { h } = await buildHandler(); const cb = (h.user.subscribe.update as jest.Mock).mock.calls[0][0]; cb({} as UserData); expect(updateUser).toHaveBeenCalled(); }); it("load callback llama a loadUser", async () => { const { loadUser } = await import("../api/user-api-service"); const { h } = await buildHandler(); // subscribeUserEvents registra su propio load callback (segundo llamado a subscribe.load) const allCalls = (h.user.subscribe.load as jest.Mock).mock.calls; const userEventsCb = allCalls[allCalls.length - 1][0]; userEventsCb({} as UserData); expect(loadUser).toHaveBeenCalled(); }); it("delete callback llama a deleteUSer", async () => { const { deleteUSer } = await import("../api/user-api-service"); const { h } = await buildHandler(); const cb = (h.user.subscribe.delete as jest.Mock).mock.calls[0][0]; cb({} as UserData); expect(deleteUSer).toHaveBeenCalled(); }); }); // ── Callbacks de Account ────────────────────────────────────────────────────── describe("BackendHandler - callbacks de Account", () => { it("creation callback llama a createAccount", async () => { const { createAccount } = await import("../api/account-api-service"); const { h } = await buildHandler(); const cb = (h.account.subscribe.creation as jest.Mock).mock.calls[0][0]; cb({ id: "a1" } as Account); expect(createAccount).toHaveBeenCalled(); }); it("update callback llama a updateAccount", async () => { const { updateAccount } = await import("../api/account-api-service"); const { h } = await buildHandler(); const cb = (h.account.subscribe.update as jest.Mock).mock.calls[0][0]; cb({ id: "a1" } as Account); expect(updateAccount).toHaveBeenCalled(); }); it("delete callback llama a deleteAccount", async () => { const { deleteAccount } = await import("../api/account-api-service"); const { h } = await buildHandler(); const cb = (h.account.subscribe.delete as jest.Mock).mock.calls[0][0]; cb({ id: "a1" } as Account); expect(deleteAccount).toHaveBeenCalled(); }); it("clean callback llama a clearAccount", async () => { const { clearAccount } = await import("../api/account-api-service"); const { h } = await buildHandler(); const cb = (h.account.subscribe.clean as jest.Mock).mock.calls[0][0]; cb({ id: "a1" } as Account); expect(clearAccount).toHaveBeenCalled(); }); }); // ── Callbacks de Plan ───────────────────────────────────────────────────────── describe("BackendHandler - callbacks de Plan", () => { it("upgraded callback publica notificación de plan-upgrade", async () => { const { h } = await buildHandler(); const cb = (h.plan.subscribe.upgraded as jest.Mock).mock.calls[0][0]; cb("premium"); expect(h.notification.publish.creation).toHaveBeenCalledWith( expect.objectContaining({ type: "success", subtype: "plan-upgrade", data: { plan: "premium" } }), ); }); it("downgraded callback publica notificación de plan-downgrade", async () => { const { h } = await buildHandler(); const cb = (h.plan.subscribe.downgraded as jest.Mock).mock.calls[0][0]; cb("freemium"); expect(h.notification.publish.creation).toHaveBeenCalledWith( expect.objectContaining({ type: "success", subtype: "plan-downgrade", data: { plan: "freemium" } }), ); }); it("upgrade y downgrade empty callbacks no lanzan error", async () => { const { h } = await buildHandler(); const upgradeCb = (h.plan.subscribe.upgrade as jest.Mock).mock.calls[0][0]; const upgradeErrorCb = (h.plan.subscribe.upgradeError as jest.Mock).mock.calls[0][0]; const downgradeCb = (h.plan.subscribe.downgrade as jest.Mock).mock.calls[0][0]; const downgradeErrorCb = (h.plan.subscribe.downgradeError as jest.Mock).mock.calls[0][0]; expect(() => upgradeCb("p")).not.toThrow(); expect(() => upgradeErrorCb("p")).not.toThrow(); expect(() => downgradeCb("p")).not.toThrow(); expect(() => downgradeErrorCb("p")).not.toThrow(); }); }); // ── Callbacks de Environment ────────────────────────────────────────────────── describe("BackendHandler - callbacks de Environment", () => { const mockEnv: Environment = { id: "e1", name: "Env1", account: "acc1", tenant: "ten1", logo: "", services: [], domains: [], status: { code: "", message: "", timestamp: "" }, usage: { current: { cpu: 0, memory: 0, storage: 0, volatileStorage: 0, nonReplicatedStorage: 0, persistentStorage: 0 }, limit: { cpu: { max: 0, min: 0 }, memory: { max: 0, min: 0 }, storage: { max: 0, min: 0 }, volatileStorage: { max: 0, min: 0 }, nonReplicatedStorage: { max: 0, min: 0 }, persistentStorage: { max: 0, min: 0 }, }, cost: 0, }, type: "", cluster: { name: "" }, }; it("creation callback llama a createEnvironment", async () => { const { createEnvironment } = await import("../api/environment-api-service"); const { h } = await buildHandler(); const cb = (h.environment.subscribe.creation as jest.Mock).mock.calls[0][0]; cb(mockEnv); expect(createEnvironment).toHaveBeenCalled(); }); it("created callback publica notificación environment-created", async () => { const { h } = await buildHandler(); const cb = (h.environment.subscribe.created as jest.Mock).mock.calls[0][0]; cb(mockEnv); expect(h.notification.publish.creation).toHaveBeenCalledWith( expect.objectContaining({ type: "success", subtype: "environment-created", data: { environment: "Env1", account: "acc1", tenant: "ten1" } }), ); }); it("update callback llama a updateEnvironment", async () => { const { updateEnvironment } = await import("../api/environment-api-service"); const { h } = await buildHandler(); const cb = (h.environment.subscribe.update as jest.Mock).mock.calls[0][0]; cb(mockEnv); expect(updateEnvironment).toHaveBeenCalled(); }); it("delete callback llama a deleteEnvironment", async () => { const { deleteEnvironment } = await import("../api/environment-api-service"); const { h } = await buildHandler(); const cb = (h.environment.subscribe.delete as jest.Mock).mock.calls[0][0]; cb(mockEnv); expect(deleteEnvironment).toHaveBeenCalled(); }); it("clean callback llama a clearEnvironment", async () => { const { clearEnvironment } = await import("../api/environment-api-service"); const { h } = await buildHandler(); const cb = (h.environment.subscribe.clean as jest.Mock).mock.calls[0][0]; cb(mockEnv); expect(clearEnvironment).toHaveBeenCalled(); }); it("scale callback llama a scaleEnvironment", async () => { const { scaleEnvironment } = await import("../api/environment-api-service"); const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {}); const { h } = await buildHandler(); const cb = (h.environment.subscribe.scale as jest.Mock).mock.calls[0][0]; cb(mockEnv); expect(scaleEnvironment).toHaveBeenCalled(); consoleSpy.mockRestore(); }); }); // ── Callbacks de Service ────────────────────────────────────────────────────── describe("BackendHandler - callbacks de Service", () => { const mockService: Service = { id: "s1", tenant: "ten1", account: "acc1", environment: "env1", name: "Svc1", logo: "", description: "", revisions: [], status: "", role: [{ name: "r1", instances: [] }], links: [], resources: [], parameters: [], usage: { current: { cpu: 0, memory: 0, storage: 0, volatileStorage: 0, nonReplicatedStorage: 0, persistentStorage: 0 }, limit: { cpu: { max: 0, min: 0 }, memory: { max: 0, min: 0 }, storage: { max: 0, min: 0 }, volatileStorage: { max: 0, min: 0 }, nonReplicatedStorage: { max: 0, min: 0 }, persistentStorage: { max: 0, min: 0 }, }, cost: 0, }, project: "", registry: "", imageName: "", entrypoint: "", cmd: "", serverChannels: [], clientChannels: [], duplexChannels: [], cloudProvider: "", }; it("deployed callback publica notificación deployment-success", async () => { const { h } = await buildHandler(); const cb = (h.service.subscribe.deployed as jest.Mock).mock.calls[0][0]; cb(mockService); expect(h.notification.publish.creation).toHaveBeenCalledWith( expect.objectContaining({ type: "success", subtype: "deployment-success" }), ); }); it("deploymentError callback publica notificación deployment-error con error info", async () => { const { h } = await buildHandler(); const cb = (h.service.subscribe.deploymentError as jest.Mock).mock.calls[0][0]; const svcWithErr = { ...mockService, error: { code: "500", message: "err", timestamp: "now" } }; cb(svcWithErr); expect(h.notification.publish.creation).toHaveBeenCalledWith( expect.objectContaining({ type: "error", subtype: "deployment-error", info_content: { code: "500", message: "err", timestamp: "now" } }), ); }); it("deploymentError callback maneja service sin error (defaults vacíos)", async () => { const { h } = await buildHandler(); const cb = (h.service.subscribe.deploymentError as jest.Mock).mock.calls[0][0]; cb(mockService); expect(h.notification.publish.creation).toHaveBeenCalledWith( expect.objectContaining({ info_content: { code: "", message: "", timestamp: "" } }), ); }); it("deploy callback llama a deployService y publica notification cuando download es falsy", async () => { const { deployService } = await import("../api/service-api-service"); const { h } = await buildHandler(); const cb = (h.service.subscribe.deploy as jest.Mock).mock.calls[0][0]; await cb({ ...mockService, download: false }); expect(deployService).toHaveBeenCalled(); expect(h.notification.publish.creation).toHaveBeenCalledWith( expect.objectContaining({ type: "info", subtype: "deployment-in-progress" }), ); }); it("deploy callback no publica notificación cuando download es true", async () => { const { h } = await buildHandler(); const cb = (h.service.subscribe.deploy as jest.Mock).mock.calls[0][0]; (h.notification.publish.creation as jest.Mock).mockClear(); await cb({ ...mockService, download: true }); expect(h.notification.publish.creation).not.toHaveBeenCalled(); }); it("update callback llama a updateService", async () => { const { updateService } = await import("../api/service-api-service"); const { h } = await buildHandler(); const cb = (h.service.subscribe.update as jest.Mock).mock.calls[0][0]; cb(mockService); expect(updateService).toHaveBeenCalled(); }); it("updated callback publica notificación deployment-updated", async () => { const { h } = await buildHandler(); const cb = (h.service.subscribe.updated as jest.Mock).mock.calls[0][0]; cb(mockService); expect(h.notification.publish.creation).toHaveBeenCalledWith( expect.objectContaining({ type: "success", subtype: "deployment-updated" }), ); }); it("delete callback publica notificación deployment-deleting y llama a deleteService", async () => { const { deleteService } = await import("../api/service-api-service"); const { h } = await buildHandler(); const cb = (h.service.subscribe.delete as jest.Mock).mock.calls[0][0]; cb(mockService); expect(h.notification.publish.creation).toHaveBeenCalledWith( expect.objectContaining({ type: "info", subtype: "deployment-deleting" }), ); expect(deleteService).toHaveBeenCalled(); }); it("deleted callback publica notificación deployment-deleted", async () => { const { h } = await buildHandler(); const cb = (h.service.subscribe.deleted as jest.Mock).mock.calls[0][0]; cb(mockService); expect(h.notification.publish.creation).toHaveBeenCalledWith( expect.objectContaining({ type: "success", subtype: "deployment-deleted" }), ); }); it("restart callback llama a restartService", async () => { const { restartService } = await import("../api/service-api-service"); const { h } = await buildHandler(); const cb = (h.service.subscribe.restart as jest.Mock).mock.calls[0][0]; cb(mockService); expect(restartService).toHaveBeenCalled(); }); it("requestRevisionData callback llama a requestRevisionData", async () => { const { requestRevisionData } = await import("../api/service-api-service"); const { h } = await buildHandler(); const cb = (h.service.subscribe.requestRevisionData as jest.Mock).mock.calls[0][0]; cb(mockService); expect(requestRevisionData).toHaveBeenCalled(); }); it("updateServiceLinks callback llama a updateServiceLinks", async () => { const { updateServiceLinks } = await import("../api/service-api-service"); const { h } = await buildHandler(); const cb = (h.service.subscribe.updateServiceLinks as jest.Mock).mock.calls[0][0]; cb({ id: "link1" } as any); expect(updateServiceLinks).toHaveBeenCalled(); }); it("changeRevision callback llama a changeRevision", async () => { const { changeRevision } = await import("../api/service-api-service"); const { h } = await buildHandler(); const cb = (h.service.subscribe.changeRevision as jest.Mock).mock.calls[0][0]; cb(mockService); expect(changeRevision).toHaveBeenCalled(); }); it("restartInstance callback llama a restartInstance", async () => { const { restartInstance } = await import("../api/service-api-service"); const { h } = await buildHandler(); const cb = (h.service.subscribe.restartInstance as jest.Mock).mock.calls[0][0]; cb({ service: mockService, roleId: "r1", instanceId: "i1" }); expect(restartInstance).toHaveBeenCalledWith(mockService, "r1", "i1", ""); }); it("empty callbacks de service no lanzan error", async () => { const { h } = await buildHandler(); const names = ["deletionError", "updateError", "requestLogs"]; for (const name of names) { const cb = (h.service.subscribe[name as keyof typeof h.service.subscribe] as jest.Mock).mock.calls[0][0]; expect(() => cb(mockService)).not.toThrow(); } }); }); // ── Callbacks de Marketplace ────────────────────────────────────────────────── describe("BackendHandler - callbacks de Marketplace", () => { it("deployItem callback llama a deployMarketplaceItem", async () => { const { deployMarketplaceItem } = await import("../api/marketplace-api-service"); const { h } = await buildHandler(); const cb = (h.marketplace.subscribe.deployItem as jest.Mock).mock.calls[0][0]; cb({ deploymentData: { name: "item1" } } as MarketplaceService); expect(deployMarketplaceItem).toHaveBeenCalled(); }); it("loadSchema callback exitoso publica schemaLoaded", async () => { const { fetchAndStoreMarketplaceSchema } = await import("../websocket-manager"); (fetchAndStoreMarketplaceSchema as jest.Mock).mockResolvedValue(undefined); const { h } = await buildHandler(); const cb = (h.marketplace.subscribe.loadSchema as jest.Mock).mock.calls[0][0]; const item: MarketplaceItem = { name: "item1" } as any; await cb(JSON.stringify({ item })); expect(fetchAndStoreMarketplaceSchema).toHaveBeenCalledWith(item); expect(h.marketplace.publish.schemaLoaded).toHaveBeenCalledWith(item); }); it("loadSchema callback con error publica schemaLoadError", async () => { const { fetchAndStoreMarketplaceSchema } = await import("../websocket-manager"); (fetchAndStoreMarketplaceSchema as jest.Mock).mockRejectedValue(new Error("schema-err")); const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {}); const { h } = await buildHandler(); const cb = (h.marketplace.subscribe.loadSchema as jest.Mock).mock.calls[0][0]; const payload = JSON.stringify({ item: { name: "item1" } }); await cb(payload); expect(h.marketplace.publish.schemaLoadError).toHaveBeenCalledWith(payload); consoleSpy.mockRestore(); }); it("loadSchema callback con JSON inválido publica schemaLoadError", async () => { const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {}); const { h } = await buildHandler(); const cb = (h.marketplace.subscribe.loadSchema as jest.Mock).mock.calls[0][0]; await cb("not-valid-json"); expect(h.marketplace.publish.schemaLoadError).toHaveBeenCalled(); consoleSpy.mockRestore(); }); it("loadItems callback no lanza error", async () => { const { h } = await buildHandler(); const cb = (h.marketplace.subscribe.loadItems as jest.Mock).mock.calls[0][0]; expect(() => cb(["t1", "t2"])).not.toThrow(); }); it("empty callbacks de marketplace no lanzan error", async () => { const { h } = await buildHandler(); const names = ["itemDeployed", "deploymentError", "updateItem", "itemUpdated", "updateError", "deleteItem", "itemDeleted", "deletionError"]; for (const name of names) { const cb = (h.marketplace.subscribe[name as keyof typeof h.marketplace.subscribe] as jest.Mock).mock.calls[0][0]; expect(() => cb({})).not.toThrow(); } }); }); // ── Callbacks de Resource ───────────────────────────────────────────────────── describe("BackendHandler - callbacks de Resource", () => { const mockResource: Resource = { name: "res1", type: "domain", tenant: "ten1" } as any; it("creation callback llama a createResource", async () => { const { createResource } = await import("../api/resources-api-service"); const { h } = await buildHandler(); const cb = (h.resource.subscribe.creation as jest.Mock).mock.calls[0][0]; cb(mockResource); expect(createResource).toHaveBeenCalledWith("ten1", mockResource, ""); }); it("creation callback con tenant undefined usa string vacío", async () => { const { createResource } = await import("../api/resources-api-service"); const { h } = await buildHandler(); const cb = (h.resource.subscribe.creation as jest.Mock).mock.calls[0][0]; cb({ name: "r1", type: "domain" } as Resource); expect(createResource).toHaveBeenCalledWith("", expect.anything(), ""); }); it("update callback llama a updateResource", async () => { const { updateResource } = await import("../api/resources-api-service"); const { h } = await buildHandler(); const cb = (h.resource.subscribe.update as jest.Mock).mock.calls[0][0]; cb(mockResource); expect(updateResource).toHaveBeenCalled(); }); it("delete callback llama a deleteResource", async () => { const { deleteResource } = await import("../api/resources-api-service"); const { h } = await buildHandler(); const cb = (h.resource.subscribe.delete as jest.Mock).mock.calls[0][0]; cb(mockResource); expect(deleteResource).toHaveBeenCalled(); }); it("deleted callback publica notificación resource-deleted", async () => { const { h } = await buildHandler(); const cb = (h.resource.subscribe.deleted as jest.Mock).mock.calls[0][0]; cb({ name: "res1", type: "domain", tenant: "ten1" } as any); expect(h.notification.publish.creation).toHaveBeenCalledWith( expect.objectContaining({ type: "success", subtype: "resource-deleted", data: { resource: "res1", type: "domain", tenant: "ten1" } }), ); }); it("empty callbacks de resource no lanzan error", async () => { const { h } = await buildHandler(); const names = ["created", "creationError", "updated", "updateError", "deletionError"]; for (const name of names) { const cb = (h.resource.subscribe[name as keyof typeof h.resource.subscribe] as jest.Mock).mock.calls[0][0]; expect(() => cb({})).not.toThrow(); } }); }); // ── Callbacks de Organization ───────────────────────────────────────────────── describe("BackendHandler - callbacks de Organization", () => { it("todos los callbacks de organization no lanzan error", async () => { const { h } = await buildHandler(); const names = ["creation", "created", "creationError", "update", "updated", "updateError", "delete", "deleted", "deletionError"]; for (const name of names) { const cb = (h.organization.subscribe[name as keyof typeof h.organization.subscribe] as jest.Mock).mock.calls[0][0]; expect(() => cb({})).not.toThrow(); } }); }); // ── Registry, invite y token empty callbacks ────────────────────────────────── describe("BackendHandler - empty callbacks adicionales", () => { it("tenant registry empty callbacks no lanzan error", async () => { const { h } = await buildHandler(); const names = ["registryCreated", "registryCreationError", "registryUpdated", "registryUpdateError", "registryDeleted", "registryDeletionError"]; for (const name of names) { const cb = (h.tenant.subscribe[name as keyof typeof h.tenant.subscribe] as jest.Mock).mock.calls[0][0]; expect(() => cb({})).not.toThrow(); } }); it("tenant invite/user empty callbacks no lanzan error", async () => { const { h } = await buildHandler(); const names = ["inviteError", "userRemoved", "removeUserError", "inviteUpdated", "inviteUpdateError", "inviteAccepted", "acceptInviteError", "inviteRejected", "inviteRejectError"]; for (const name of names) { const cb = (h.tenant.subscribe[name as keyof typeof h.tenant.subscribe] as jest.Mock).mock.calls[0][0]; expect(() => cb({})).not.toThrow(); } }); it("tenant token non-subscribed mocks tienen cero llamadas", async () => { const { h } = await buildHandler(); // tokenCreated/tokenCreationError/tokenDeleted/tokenDeletionError no son suscritos por BackendHandler expect((h.tenant.subscribe.tokenCreated as jest.Mock).mock.calls).toHaveLength(0); expect((h.tenant.subscribe.tokenDeleted as jest.Mock).mock.calls).toHaveLength(0); }); it("user empty callbacks no lanzan error", async () => { const { h } = await buildHandler(); const names = ["created", "creationError", "updated", "updateError", "loadError", "deleted", "deletionError"]; for (const name of names) { const cb = (h.user.subscribe[name as keyof typeof h.user.subscribe] as jest.Mock).mock.calls[0][0]; expect(() => cb({})).not.toThrow(); } }); it("account empty callbacks no lanzan error", async () => { const { h } = await buildHandler(); const names = ["created", "creationError", "updated", "updateError", "deleted", "deletionError"]; for (const name of names) { const cb = (h.account.subscribe[name as keyof typeof h.account.subscribe] as jest.Mock).mock.calls[0][0]; expect(() => cb({})).not.toThrow(); } }); it("environment empty callbacks no lanzan error", async () => { const { h } = await buildHandler(); const names = ["creationError", "updated", "updateError", "deleted", "deletionError"]; for (const name of names) { const cb = (h.environment.subscribe[name as keyof typeof h.environment.subscribe] as jest.Mock).mock.calls[0][0]; expect(() => cb({})).not.toThrow(); } }); });