import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ audit: vi.fn(), notify: vi.fn(), tx: { employeePasswordSafeVault: { findFirst: vi.fn(), findMany: vi.fn() }, employeePasswordSafeAccess: { findFirst: vi.fn(), findMany: vi.fn(), count: vi.fn(), upsert: vi.fn(), deleteMany: vi.fn(), }, employeePasswordSafeEntry: { findFirst: vi.fn(), findMany: vi.fn(), updateMany: vi.fn() }, tenantUser: { findFirst: vi.fn(), findMany: vi.fn() }, userRole: { findMany: vi.fn() }, auditEvent: { findMany: 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: mocks.audit })); vi.mock("@hivegrid/foundation", async (importOriginal) => ({ ...(await importOriginal()), notify: mocks.notify, })); import { archiveEntry, copyEntryValue, exportVault, getVaultForUser, grantAccess, listAccessibleVaults, listPasswordSafeActivity, revokeAccess, } from "./hrPasswordSafeService.server"; const crypto = { encrypt: vi.fn(async (_tenantId: string, value: string) => `cipher:${value}`), decrypt: vi.fn(async (_tenantId: string, value: string) => value.replace("cipher:", "")), }; describe("password-safe object permissions and command auditing", () => { beforeEach(() => { vi.clearAllMocks(); mocks.tx.employeePasswordSafeVault.findFirst.mockResolvedValue({ id: "vault-1", tenantId: "tenant-1", name: "Ops", archivedAt: null }); mocks.tx.employeePasswordSafeVault.findMany.mockResolvedValue([]); mocks.tx.employeePasswordSafeAccess.findFirst.mockResolvedValue({ permissionLevel: "admin" }); mocks.tx.employeePasswordSafeAccess.findMany.mockResolvedValue([]); mocks.tx.employeePasswordSafeAccess.count.mockResolvedValue(2); mocks.tx.employeePasswordSafeAccess.upsert.mockResolvedValue({ id: "access-1" }); mocks.tx.employeePasswordSafeAccess.deleteMany.mockResolvedValue({ count: 1 }); mocks.tx.employeePasswordSafeEntry.findMany.mockResolvedValue([]); mocks.tx.employeePasswordSafeEntry.updateMany.mockResolvedValue({ count: 1 }); mocks.tx.tenantUser.findFirst.mockResolvedValue({ id: "member-1" }); mocks.tx.tenantUser.findMany.mockResolvedValue([]); mocks.tx.userRole.findMany.mockResolvedValue([]); mocks.tx.auditEvent.findMany.mockResolvedValue([]); mocks.audit.mockResolvedValue({ id: "audit-1" }); mocks.notify.mockResolvedValue({ id: "notification-1" }); }); it("rejects a grant target outside the active tenant membership", async () => { mocks.tx.tenantUser.findFirst.mockResolvedValue(null); await expect( grantAccess({ tenantId: "tenant-1", vaultId: "vault-1", targetUserId: "outsider", permissionLevel: "read", actorUserId: "admin-1" }) ).rejects.toThrow(); expect(mocks.tx.employeePasswordSafeAccess.upsert).not.toHaveBeenCalled(); expect(mocks.audit).not.toHaveBeenCalled(); }); it("prevents downgrading or revoking the last vault admin", async () => { mocks.tx.employeePasswordSafeAccess.count.mockResolvedValue(1); await expect( grantAccess({ tenantId: "tenant-1", vaultId: "vault-1", targetUserId: "admin-1", permissionLevel: "write", actorUserId: "admin-1" }) ).rejects.toMatchObject({ messageKey: "hr.error.lastVaultAdmin" }); expect(mocks.tx.employeePasswordSafeAccess.upsert).not.toHaveBeenCalled(); mocks.tx.employeePasswordSafeAccess.findFirst.mockResolvedValue({ permissionLevel: "admin" }); await expect( revokeAccess({ tenantId: "tenant-1", vaultId: "vault-1", targetUserId: "admin-1", actorUserId: "admin-1" }) ).rejects.toMatchObject({ messageKey: "hr.error.lastVaultAdmin" }); expect(mocks.tx.employeePasswordSafeAccess.deleteMany).not.toHaveBeenCalled(); }); it("audits and notifies a named access change without secret material", async () => { mocks.tx.employeePasswordSafeAccess.findFirst .mockResolvedValueOnce({ permissionLevel: "admin" }) .mockResolvedValueOnce(null); await grantAccess({ tenantId: "tenant-1", vaultId: "vault-1", targetUserId: "member-1", permissionLevel: "read", actorUserId: "admin-1" }); expect(mocks.audit).toHaveBeenCalledWith(expect.objectContaining({ action: "HR_PASSWORD_SAFE_ACCESS_GRANT", dataSensitivity: "internal" })); expect(mocks.notify).toHaveBeenCalledWith( expect.objectContaining({ userId: "member-1", messageKey: "hr.passwordSafe.notification.accessGranted" }), mocks.tx ); expect(JSON.stringify(mocks.audit.mock.calls)).not.toMatch(/passwordCiphertext|usernameCiphertext|notesCiphertext/u); }); it("copy re-checks read access and writes a distinct confidential audit event", async () => { mocks.tx.employeePasswordSafeAccess.findFirst.mockResolvedValue({ permissionLevel: "read" }); mocks.tx.employeePasswordSafeEntry.findFirst.mockResolvedValue({ id: "entry-1", usernameCiphertext: "cipher:alice", passwordCiphertext: "cipher:secret", notesCiphertext: null, }); await expect( copyEntryValue({ tenantId: "tenant-1", vaultId: "vault-1", entryId: "entry-1", userId: "member-1", field: "password" }, crypto) ).resolves.toBe("secret"); expect(mocks.audit).toHaveBeenCalledWith( expect.objectContaining({ action: "HR_PASSWORD_SAFE_COPY", dataSensitivity: "confidential", metadata: { vaultId: "vault-1", field: "password" } }) ); }); it("enforces read, write and admin as separate object roles and grants no content access to a metadata admin", async () => { mocks.tx.employeePasswordSafeAccess.findFirst.mockResolvedValue({ permissionLevel: "read" }); await expect( archiveEntry({ tenantId: "tenant-1", vaultId: "vault-1", entryId: "entry-1", userId: "reader-1" }) ).rejects.toThrow(); expect(mocks.tx.employeePasswordSafeEntry.updateMany).not.toHaveBeenCalled(); mocks.tx.employeePasswordSafeAccess.findFirst.mockResolvedValue({ permissionLevel: "write" }); await expect( archiveEntry({ tenantId: "tenant-1", vaultId: "vault-1", entryId: "entry-1", userId: "writer-1" }) ).resolves.toBeUndefined(); await expect( grantAccess({ tenantId: "tenant-1", vaultId: "vault-1", targetUserId: "member-1", permissionLevel: "read", actorUserId: "writer-1" }) ).rejects.toThrow(); mocks.tx.employeePasswordSafeAccess.findFirst.mockResolvedValue(null); await expect(getVaultForUser("tenant-1", "vault-1", "metadata-admin-1")).resolves.toBeNull(); }); it("keeps metadata search access-scoped and never queries entry secrets", async () => { await listAccessibleVaults("tenant-1", "member-1", { search: "router" }); expect(mocks.tx.employeePasswordSafeVault.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: { tenantId: "tenant-1", archivedAt: null, access: { some: { tenantId: "tenant-1", userId: "member-1" } }, OR: [ { name: { contains: "router", mode: "insensitive" } }, { description: { contains: "router", mode: "insensitive" } }, ], }, }) ); expect(mocks.tx.employeePasswordSafeEntry.findMany).not.toHaveBeenCalled(); }); it("returns a redacted activity timeline without audit metadata or raw actor ids", async () => { mocks.tx.auditEvent.findMany.mockResolvedValue([{ id: "audit-1", createdAt: new Date("2026-07-25T10:00:00Z"), action: "HR_PASSWORD_SAFE_REVEAL", actorId: "opaque-user-id", outcome: "success", metadata: { field: "password", entryId: "secret-entry-id" }, }]); mocks.tx.tenantUser.findMany.mockResolvedValue([{ userId: "opaque-user-id", user: { firstName: "Mira", lastName: "Beispiel", email: "mira@example.test" }, }]); const timeline = await listPasswordSafeActivity({ tenantId: "tenant-1", userId: "metadata-admin-1", isTenantPasswordSafeAdmin: true, }); expect(timeline).toEqual([expect.objectContaining({ action: "HR_PASSWORD_SAFE_REVEAL", actorLabel: "Mira Beispiel" })]); expect(JSON.stringify(timeline)).not.toMatch(/opaque-user-id|secret-entry-id|entryId|field/u); }); it("fails closed in the service when the separate export permission is absent", async () => { await expect( exportVault({ tenantId: "tenant-1", vaultId: "vault-1", userId: "member-1", hasExportPermission: false }, crypto) ).rejects.toThrow(); expect(mocks.tx.employeePasswordSafeEntry.findMany).not.toHaveBeenCalled(); expect(mocks.audit).not.toHaveBeenCalled(); }); it("exports only with the separate permission and records a confidential GDPR audit event", async () => { mocks.tx.employeePasswordSafeAccess.findFirst.mockResolvedValue({ permissionLevel: "read" }); await expect( exportVault({ tenantId: "tenant-1", vaultId: "vault-1", userId: "member-1", hasExportPermission: true }, crypto) ).resolves.toEqual({ vaultName: "Ops", entries: [] }); expect(mocks.audit).toHaveBeenCalledWith( expect.objectContaining({ action: "HR_PASSWORD_SAFE_EXPORT", dataSensitivity: "confidential", regulatoryScope: ["gdpr"], }) ); }); });