139 lines
5.0 KiB
TypeScript
139 lines
5.0 KiB
TypeScript
// @vitest-environment node
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("@toolctrl/config", async () => {
|
|
const actual = await vi.importActual<typeof import("@toolctrl/config")>("@toolctrl/config");
|
|
return {
|
|
...actual,
|
|
loadRequiredWebConfig: () => ({ nodeEnv: "production" }),
|
|
};
|
|
});
|
|
vi.mock("./db/tenants.db.server", () => ({
|
|
getTenantUserType: vi.fn(),
|
|
}));
|
|
vi.mock("./services/.server/urlService", async () => {
|
|
const actual = await vi.importActual<typeof import("./services/.server/urlService")>("./services/.server/urlService");
|
|
return {
|
|
...actual,
|
|
getTenantIdFromUrl: vi.fn(),
|
|
};
|
|
});
|
|
vi.mock("./session.server", () => ({
|
|
getUserInfo: vi.fn(),
|
|
}));
|
|
vi.mock("./db.server", () => ({
|
|
db: {
|
|
adminUser: {
|
|
findUnique: vi.fn(),
|
|
},
|
|
},
|
|
}));
|
|
vi.mock("./db/permissions/userRoles.db.server", () => ({
|
|
getUserRoleInTenant: vi.fn(),
|
|
}));
|
|
vi.mock("./cache.server", () => ({
|
|
cachified: vi.fn(),
|
|
clearCacheKey: vi.fn(),
|
|
}));
|
|
|
|
import { getTenantUserType } from "./db/tenants.db.server";
|
|
import { db } from "./db.server";
|
|
import { getUserRoleInTenant } from "./db/permissions/userRoles.db.server";
|
|
import { getTenantIdFromUrl } from "./services/.server/urlService";
|
|
import { getUserInfo } from "./session.server";
|
|
import { requireAuth } from "./loaders.middleware";
|
|
|
|
const env = {
|
|
APP_BASE_DOMAIN: "localhost:3000",
|
|
WEB_APP_HOST: "app.localhost:3000",
|
|
ADMIN_APP_HOST: "admin.localhost:3000",
|
|
API_HOST: "api.localhost:3000",
|
|
APP_PROTOCOL: "http",
|
|
};
|
|
|
|
const userSession = { userId: "user-1", lightOrDarkMode: "light", lng: "en", cookies: [] };
|
|
|
|
function makeRequest(host: string, pathname: string) {
|
|
return new Request(`http://localhost:3000${pathname}`, {
|
|
headers: {
|
|
host,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function expectUnauthorized(promise: Promise<unknown>) {
|
|
try {
|
|
await promise;
|
|
throw new Error("Expected unauthorized response");
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(Response);
|
|
expect((error as Response).status).toBe(401);
|
|
}
|
|
}
|
|
|
|
describe("requireAuth tenant-source routing", () => {
|
|
beforeEach(() => {
|
|
vi.stubEnv("APP_BASE_DOMAIN", env.APP_BASE_DOMAIN);
|
|
vi.stubEnv("WEB_APP_HOST", env.WEB_APP_HOST);
|
|
vi.stubEnv("ADMIN_APP_HOST", env.ADMIN_APP_HOST);
|
|
vi.stubEnv("API_HOST", env.API_HOST);
|
|
vi.stubEnv("APP_PROTOCOL", env.APP_PROTOCOL);
|
|
vi.clearAllMocks();
|
|
vi.mocked(getUserInfo).mockResolvedValue(userSession);
|
|
vi.mocked(getTenantIdFromUrl).mockResolvedValue("tenant-1");
|
|
vi.mocked(getTenantUserType).mockResolvedValue({ status: 2, type: 1 });
|
|
vi.mocked(db.adminUser.findUnique).mockResolvedValue(null);
|
|
vi.mocked(getUserRoleInTenant).mockResolvedValue(null);
|
|
});
|
|
|
|
it("requires tenant membership for app-host tenant route params", async () => {
|
|
await expect(requireAuth({ request: makeRequest(env.WEB_APP_HOST, "/acme/dashboard"), params: { tenant: "acme" } })).resolves.toEqual(
|
|
userSession,
|
|
);
|
|
|
|
expect(getTenantIdFromUrl).toHaveBeenCalledWith({ tenant: "acme" });
|
|
expect(getTenantUserType).toHaveBeenCalledWith("user-1", "tenant-1");
|
|
});
|
|
|
|
it("rejects non-member app-host tenant requests", async () => {
|
|
vi.mocked(getTenantUserType).mockResolvedValueOnce(null);
|
|
|
|
await expectUnauthorized(requireAuth({ request: makeRequest(env.WEB_APP_HOST, "/acme/dashboard"), params: { tenant: "acme" } }));
|
|
|
|
expect(getTenantUserType).toHaveBeenCalledWith("user-1", "tenant-1");
|
|
expect(db.adminUser.findUnique).toHaveBeenCalledWith({ where: { userId: "user-1" } });
|
|
});
|
|
|
|
it("allows operator admins to inspect app-host tenant requests without membership", async () => {
|
|
vi.mocked(getTenantUserType).mockResolvedValueOnce(null);
|
|
vi.mocked(db.adminUser.findUnique).mockResolvedValueOnce({ userId: "user-1" });
|
|
|
|
await expect(requireAuth({ request: makeRequest(env.WEB_APP_HOST, "/acme/dashboard"), params: { tenant: "acme" } })).resolves.toEqual(userSession);
|
|
|
|
expect(getTenantUserType).toHaveBeenCalledWith("user-1", "tenant-1");
|
|
expect(db.adminUser.findUnique).toHaveBeenCalledWith({ where: { userId: "user-1" } });
|
|
});
|
|
|
|
it("keeps admin-host paths on the super-admin branch", async () => {
|
|
vi.mocked(db.adminUser.findUnique).mockResolvedValueOnce({ userId: "user-1" });
|
|
|
|
await expect(requireAuth({ request: makeRequest(env.ADMIN_APP_HOST, "/settings"), params: {} })).resolves.toEqual({
|
|
...userSession,
|
|
isSuperAdmin: { userId: "user-1" },
|
|
});
|
|
|
|
expect(db.adminUser.findUnique).toHaveBeenCalledWith({ where: { userId: "user-1" } });
|
|
expect(getTenantUserType).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not classify admin-prefixed non-admin paths as admin host routes", async () => {
|
|
await expect(requireAuth({ request: makeRequest(env.WEB_APP_HOST, "/adminish/dashboard"), params: { tenant: "adminish" } })).resolves.toEqual(
|
|
userSession,
|
|
);
|
|
|
|
expect(getTenantIdFromUrl).toHaveBeenCalledWith({ tenant: "adminish" });
|
|
expect(getTenantUserType).toHaveBeenCalledWith("user-1", "tenant-1");
|
|
expect(db.adminUser.findUnique).not.toHaveBeenCalled();
|
|
});
|
|
});
|