106 lines
4.9 KiB
TypeScript
106 lines
4.9 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
tx: {
|
|
employeePasswordSafeVault: { findFirst: vi.fn(), findMany: vi.fn(), updateMany: vi.fn() },
|
|
employeePasswordSafeAccess: { findFirst: vi.fn() },
|
|
employeePasswordSafeEntry: { findMany: vi.fn(), updateMany: vi.fn() },
|
|
},
|
|
}));
|
|
|
|
vi.mock("~/utils/db/.server/tenantRls.server", () => ({
|
|
withTenantRLS: (_tenantId: string, callback: (tx: typeof mocks.tx) => unknown) => callback(mocks.tx),
|
|
}));
|
|
|
|
vi.mock("@hivegrid/db/audit-events.server", () => ({ createAuditEvent: vi.fn() }));
|
|
|
|
import { archiveVault, listAccessibleVaults, listEntries, restoreEntry, restoreVault } from "./hrPasswordSafeService.server";
|
|
|
|
describe("password-safe archive lifecycle", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mocks.tx.employeePasswordSafeVault.findFirst.mockResolvedValue({ id: "vault-1" });
|
|
mocks.tx.employeePasswordSafeAccess.findFirst.mockResolvedValue({ permissionLevel: "write" });
|
|
mocks.tx.employeePasswordSafeEntry.findMany.mockResolvedValue([]);
|
|
mocks.tx.employeePasswordSafeEntry.updateMany.mockResolvedValue({ count: 1 });
|
|
mocks.tx.employeePasswordSafeVault.updateMany.mockResolvedValue({ count: 1 });
|
|
mocks.tx.employeePasswordSafeVault.findMany.mockResolvedValue([
|
|
{
|
|
id: "vault-1",
|
|
tenantId: "tenant-1",
|
|
name: "Operations",
|
|
description: null,
|
|
createdByUserId: "user-1",
|
|
isDemoData: false,
|
|
createdAt: new Date(0),
|
|
updatedAt: new Date(0),
|
|
archivedAt: null,
|
|
access: [{ permissionLevel: "admin" }],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("lists active and archived entries through separate metadata-only queries", async () => {
|
|
await listEntries({ tenantId: "tenant-1", vaultId: "vault-1", userId: "user-1" });
|
|
expect(mocks.tx.employeePasswordSafeEntry.findMany).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({ where: { tenantId: "tenant-1", vaultId: "vault-1", archivedAt: null } })
|
|
);
|
|
|
|
await listEntries({ tenantId: "tenant-1", vaultId: "vault-1", userId: "user-1", archived: true });
|
|
expect(mocks.tx.employeePasswordSafeEntry.findMany).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({ where: { tenantId: "tenant-1", vaultId: "vault-1", archivedAt: { not: null } } })
|
|
);
|
|
const select = mocks.tx.employeePasswordSafeEntry.findMany.mock.calls.at(-1)?.[0].select;
|
|
expect(select).not.toHaveProperty("username");
|
|
expect(select).not.toHaveProperty("password");
|
|
expect(select).not.toHaveProperty("notes");
|
|
});
|
|
|
|
it("returns the caller's actual access level with each accessible vault", async () => {
|
|
await expect(listAccessibleVaults("tenant-1", "user-1")).resolves.toEqual([expect.objectContaining({ id: "vault-1", accessLevel: "admin" })]);
|
|
expect(mocks.tx.employeePasswordSafeVault.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: { tenantId: "tenant-1", archivedAt: null, access: { some: { tenantId: "tenant-1", userId: "user-1" } } },
|
|
include: { access: { where: { tenantId: "tenant-1", userId: "user-1" }, select: { permissionLevel: true }, take: 1 } },
|
|
})
|
|
);
|
|
});
|
|
|
|
it("restores only an archived entry in the caller's tenant and vault", async () => {
|
|
await restoreEntry({ tenantId: "tenant-1", vaultId: "vault-1", entryId: "entry-1", userId: "user-1" });
|
|
expect(mocks.tx.employeePasswordSafeEntry.updateMany).toHaveBeenCalledWith({
|
|
where: {
|
|
id: "entry-1",
|
|
tenantId: "tenant-1",
|
|
vaultId: "vault-1",
|
|
archivedAt: { not: null },
|
|
},
|
|
data: { archivedAt: null },
|
|
});
|
|
});
|
|
|
|
it("does not report a missing or already-active entry as restored", async () => {
|
|
mocks.tx.employeePasswordSafeEntry.updateMany.mockResolvedValue({ count: 0 });
|
|
await expect(restoreEntry({ tenantId: "tenant-1", vaultId: "vault-1", entryId: "missing", userId: "user-1" })).rejects.toThrow();
|
|
});
|
|
|
|
it("archives and restores a vault only through inverse lifecycle filters", async () => {
|
|
await archiveVault({ tenantId: "tenant-1", vaultId: "vault-1", userId: "admin-1", isTenantPasswordSafeAdmin: true });
|
|
expect(mocks.tx.employeePasswordSafeVault.updateMany).toHaveBeenLastCalledWith({
|
|
where: { id: "vault-1", tenantId: "tenant-1", archivedAt: null },
|
|
data: { archivedAt: expect.any(Date) },
|
|
});
|
|
|
|
await restoreVault({ tenantId: "tenant-1", vaultId: "vault-1", userId: "admin-1", isTenantPasswordSafeAdmin: true });
|
|
expect(mocks.tx.employeePasswordSafeVault.updateMany).toHaveBeenLastCalledWith({
|
|
where: { id: "vault-1", tenantId: "tenant-1", archivedAt: { not: null } },
|
|
data: { archivedAt: null },
|
|
});
|
|
});
|
|
|
|
it("does not restore a vault without the tenant-wide admin capability", async () => {
|
|
await expect(restoreVault({ tenantId: "tenant-1", vaultId: "vault-1", userId: "user-1", isTenantPasswordSafeAdmin: false })).rejects.toThrow();
|
|
expect(mocks.tx.employeePasswordSafeVault.updateMany).not.toHaveBeenCalled();
|
|
});
|
|
});
|