Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/integrations/fileVisibility.server.test.ts
T

694 lines
29 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { TenantUserStatus } from "~/application/enums/tenants/TenantUserStatus";
import {
assertFileLinkTargetVisibleWithTx,
createFileVisibilityQueryState,
fileReadScopeWhereWithTx,
filterVisibleFileLinksWithTx,
type FileVisibilityContext,
} from "./fileVisibility.server";
import type { FileEntityType } from "./fileService.server";
function transaction() {
return {
fileEntityLink: { findMany: vi.fn() },
projekt: { findMany: vi.fn(), findFirst: vi.fn() },
kontakt: { findFirst: vi.fn(), findMany: vi.fn() },
inboxLead: { findFirst: vi.fn(), findMany: vi.fn() },
tenantUser: { findFirst: vi.fn(), findMany: vi.fn() },
formSubmission: { findFirst: vi.fn(), findMany: vi.fn() },
task: { findFirst: vi.fn(), findMany: vi.fn() },
appointment: { findFirst: vi.fn(), findMany: vi.fn() },
projektSignatureDocument: { findFirst: vi.fn(), findMany: vi.fn() },
userRole: { findMany: vi.fn() },
groupUser: { findMany: vi.fn() },
projektMitglied: { findMany: vi.fn() },
teamChatMessage: { findMany: vi.fn(), findFirst: vi.fn() },
};
}
describe("fileReadScopeWhereWithTx", () => {
const ownUserLink = {
links: { some: { tenantId: "tenant_1", entityType: "user", entityId: "user_1" } },
};
const nonForeignHrLink = {
links: { none: { tenantId: "tenant_1", entityType: "user", entityId: { not: "user_1" } } },
};
it("does not treat an arbitrary in-tenant entity link as files.read_own access", async () => {
const tx = transaction();
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["files.read_own"] };
await expect(fileReadScopeWhereWithTx(tx as never, "tenant_1", ctx, ["file_1"])).resolves.toEqual({
AND: [{ OR: [{ createdByUserId: "user_1" }, { assignedToUserId: "user_1" }, ownUserLink] }, nonForeignHrLink],
});
expect(tx.fileEntityLink.findMany).not.toHaveBeenCalled();
expect(tx.projekt.findMany).not.toHaveBeenCalled();
});
it("transitively authorizes only links to projects visible through membership and PipelineAccess", async () => {
const tx = transaction();
tx.fileEntityLink.findMany.mockResolvedValue([{ entityId: "project_visible" }, { entityId: "project_denied" }]);
tx.projekt.findMany.mockResolvedValue([{ id: "project_visible" }]);
const ctx: FileVisibilityContext = {
userId: "user_1",
permissions: ["files.read_own", "projects.read_own_member"],
roleIds: ["role_1"],
groupIds: ["group_1"],
};
await expect(fileReadScopeWhereWithTx(tx as never, "tenant_1", ctx, ["file_1"])).resolves.toEqual({
AND: [
{
OR: [
{ createdByUserId: "user_1" },
{ assignedToUserId: "user_1" },
{
links: {
some: {
tenantId: "tenant_1",
entityType: "project",
entityId: { in: ["project_visible"] },
},
},
},
ownUserLink,
],
},
nonForeignHrLink,
],
});
expect(tx.fileEntityLink.findMany).toHaveBeenCalledWith({
where: { tenantId: "tenant_1", entityType: "project", fileId: { in: ["file_1"] } },
select: { entityId: true },
distinct: ["entityId"],
});
expect(tx.projekt.findMany.mock.calls[0]?.[0].where).toEqual({
tenantId: "tenant_1",
id: { in: ["project_visible", "project_denied"] },
archivedAt: null,
AND: [
{
OR: [
{
mitglieder: {
some: {
tenantId: "tenant_1",
archivedAt: null,
OR: [{ userId: "user_1" }, { groupId: { in: ["group_1"] } }],
},
},
},
],
},
{
pipeline: {
accessRules: {
none: {
tenantId: "tenant_1",
kannSehen: false,
OR: [
{ principalType: "user", principalId: "user_1" },
{ principalType: "role", principalId: "role_1" },
{ principalType: "group", principalId: "group_1" },
],
},
},
},
},
],
});
});
it("keeps foreign user-linked HR files hidden even for files.read_all", async () => {
const tx = transaction();
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["files.read_all"] };
await expect(fileReadScopeWhereWithTx(tx as never, "tenant_1", ctx)).resolves.toEqual(nonForeignHrLink);
expect(tx.fileEntityLink.findMany).not.toHaveBeenCalled();
expect(tx.projekt.findMany).not.toHaveBeenCalled();
});
it("keeps PipelineAccess deny filtering when project permission is read_all", async () => {
const tx = transaction();
tx.projekt.findMany.mockResolvedValue([]);
const ctx: FileVisibilityContext = {
userId: "user_1",
permissions: ["files.read_own", "projects.read_all"],
roleIds: ["role_1"],
groupIds: ["group_1"],
};
await expect(fileReadScopeWhereWithTx(tx as never, "tenant_1", ctx)).resolves.toEqual({
AND: [{ OR: [{ createdByUserId: "user_1" }, { assignedToUserId: "user_1" }, ownUserLink] }, nonForeignHrLink],
});
expect(tx.projekt.findMany.mock.calls[0]?.[0].where).toEqual({
tenantId: "tenant_1",
archivedAt: null,
pipeline: {
accessRules: {
none: {
tenantId: "tenant_1",
kannSehen: false,
OR: [
{ principalType: "user", principalId: "user_1" },
{ principalType: "role", principalId: "role_1" },
{ principalType: "group", principalId: "group_1" },
],
},
},
},
});
expect(tx.fileEntityLink.findMany).not.toHaveBeenCalled();
});
it("hydrates project principals only when a project-linked file scope needs them", async () => {
const tx = transaction();
tx.fileEntityLink.findMany.mockResolvedValue([{ entityId: "project_1" }]);
tx.userRole.findMany.mockResolvedValue([{ roleId: "role_1" }]);
tx.groupUser.findMany.mockResolvedValue([{ groupId: "group_1" }]);
tx.projekt.findMany.mockResolvedValue([{ id: "project_1" }]);
const ctx: FileVisibilityContext = {
userId: "user_1",
permissions: ["files.read_own", "projects.read_own_member"],
};
await fileReadScopeWhereWithTx(tx as never, "tenant_1", ctx, ["file_1"]);
expect(tx.userRole.findMany).toHaveBeenCalledWith({
where: { tenantId: "tenant_1", userId: "user_1" },
select: { roleId: true },
});
expect(tx.groupUser.findMany).toHaveBeenCalledWith({
where: { userId: "user_1", group: { tenantId: "tenant_1" } },
select: { groupId: true },
});
expect(tx.projektMitglied.findMany).not.toHaveBeenCalled();
expect(tx.projekt.findMany.mock.calls[0]?.[0].where.AND[0]).toEqual({
OR: [
{
mitglieder: {
some: {
tenantId: "tenant_1",
archivedAt: null,
OR: [{ userId: "user_1" }, { groupId: { in: ["group_1"] } }],
},
},
},
],
});
expect(JSON.stringify(tx.projekt.findMany.mock.calls[0]?.[0]?.where)).toContain('"principalId":"role_1"');
});
it.each(["hr.read_all", "hr.admin"])("allows tenant-wide HR readers through the user-link override via %s", async (permission) => {
const tx = transaction();
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["files.read_all", permission] };
await expect(fileReadScopeWhereWithTx(tx as never, "tenant_1", ctx)).resolves.toEqual({});
expect(tx.fileEntityLink.findMany).not.toHaveBeenCalled();
});
it.each(["files.write", "files.admin"])("does not infer a read grant from %s", async (permission) => {
const tx = transaction();
const ctx: FileVisibilityContext = { userId: "user_1", permissions: [permission] };
await expect(fileReadScopeWhereWithTx(tx as never, "tenant_1", ctx)).resolves.toEqual({ id: { in: [] } });
});
it("does not infer the HR override from hr.write", async () => {
const tx = transaction();
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["files.read_all", "hr.write"] };
await expect(fileReadScopeWhereWithTx(tx as never, "tenant_1", ctx)).resolves.toEqual(nonForeignHrLink);
});
it("fails closed without any effective Files read permission", async () => {
const tx = transaction();
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["hr.admin"] };
await expect(fileReadScopeWhereWithTx(tx as never, "tenant_1", ctx, ["file_1"])).resolves.toEqual({ id: { in: [] } });
expect(tx.fileEntityLink.findMany).not.toHaveBeenCalled();
expect(tx.projekt.findMany).not.toHaveBeenCalled();
});
it("does not let an own user link or HR privilege replace a Files read permission", async () => {
const tx = transaction();
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["files.write", "hr.admin"] };
await expect(fileReadScopeWhereWithTx(tx as never, "tenant_1", ctx, ["own_hr_file"])).resolves.toEqual({ id: { in: [] } });
expect(tx.fileEntityLink.findMany).not.toHaveBeenCalled();
});
it("grants only attachment files linked to messages in the caller's active chats", async () => {
const tx = transaction();
tx.fileEntityLink.findMany.mockResolvedValue([{ entityId: "message_visible" }, { entityId: "message_hidden" }]);
tx.teamChatMessage.findMany.mockResolvedValue([{ id: "message_visible" }]);
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["chat.read_own_channels"] };
await expect(fileReadScopeWhereWithTx(tx as never, "tenant_1", ctx, ["file_1"])).resolves.toEqual({
AND: [
{
OR: [
{
links: {
some: {
tenantId: "tenant_1",
entityType: "chat_message",
entityId: { in: ["message_visible"] },
},
},
},
],
},
nonForeignHrLink,
],
});
expect(tx.teamChatMessage.findMany).toHaveBeenCalledWith({
where: {
tenantId: "tenant_1",
id: { in: ["message_visible", "message_hidden"] },
chat: { tenantId: "tenant_1", members: { some: { tenantId: "tenant_1", userId: "user_1", leftAt: null } } },
},
select: { id: true },
});
});
it("does not turn chat.read_all_channels into an unaudited Files download bypass", async () => {
const tx = transaction();
tx.fileEntityLink.findMany.mockResolvedValue([{ entityId: "message_visible" }, { entityId: "message_foreign" }]);
tx.teamChatMessage.findMany.mockResolvedValue([{ id: "message_visible" }]);
const ctx: FileVisibilityContext = { userId: "admin_1", permissions: ["chat.read_all_channels"] };
await fileReadScopeWhereWithTx(tx as never, "tenant_1", ctx, ["file_1"]);
expect(tx.teamChatMessage.findMany).toHaveBeenCalledWith({
where: {
tenantId: "tenant_1",
id: { in: ["message_visible", "message_foreign"] },
chat: { tenantId: "tenant_1", members: { some: { tenantId: "tenant_1", userId: "admin_1", leftAt: null } } },
},
select: { id: true },
});
});
});
describe("filterVisibleFileLinksWithTx", () => {
it("filters audit administrators' chat links by ordinary active membership", async () => {
const tx = transaction();
tx.teamChatMessage.findMany.mockResolvedValue([{ id: "message_member" }]);
const links = [
{ entityType: "chat_message" as const, entityId: "message_member", linkRole: "visible" },
{ entityType: "chat_message" as const, entityId: "message_foreign", linkRole: "hidden" },
];
await expect(
filterVisibleFileLinksWithTx(tx as never, "tenant_1", links, {
userId: "admin_1",
permissions: ["chat.read_all_channels"],
})
).resolves.toEqual([links[0]]);
expect(tx.teamChatMessage.findMany).toHaveBeenCalledWith({
where: {
id: { in: ["message_member", "message_foreign"] },
tenantId: "tenant_1",
chat: { tenantId: "tenant_1", members: { some: { tenantId: "tenant_1", userId: "admin_1", leftAt: null } } },
},
select: { id: true },
});
});
it("batches each target type, preserves source order, and fails unknown targets closed", async () => {
const tx = transaction();
tx.kontakt.findMany.mockImplementation(async ({ where }: { where: { kontaktTyp: string } }) =>
where.kontaktTyp === "privatperson" ? [{ id: "contact_visible" }] : [{ id: "company_visible" }]
);
tx.inboxLead.findMany.mockResolvedValue([{ id: "lead_visible" }]);
tx.tenantUser.findMany.mockResolvedValue([{ userId: "user_1" }]);
tx.formSubmission.findMany.mockResolvedValue([{ id: "submission_visible" }]);
tx.task.findMany.mockResolvedValue([{ id: "task_visible" }]);
tx.appointment.findMany.mockResolvedValue([{ id: "appointment_visible" }]);
const links = [
{ entityType: "task", entityId: "task_visible", linkRole: "first" },
{ entityType: "contact", entityId: "contact_hidden", linkRole: "hidden" },
{ entityType: "company", entityId: "company_visible", linkRole: "second" },
{ entityType: "email_message", entityId: "message_unknown", linkRole: "unknown" },
{ entityType: "contact", entityId: "contact_visible", linkRole: "third" },
{ entityType: "lead", entityId: "lead_visible", linkRole: "fourth" },
{ entityType: "user", entityId: "user_1", linkRole: "fifth" },
{ entityType: "form_submission", entityId: "submission_visible", linkRole: "sixth" },
{ entityType: "appointment", entityId: "appointment_visible", linkRole: "seventh" },
{ entityType: "contact", entityId: "contact_visible", linkRole: "duplicate" },
];
const ctx: FileVisibilityContext = {
userId: "user_1",
permissions: ["crm.read_all", "leads.read_all", "tasks.read_all", "forms.admin", "bookings.read_all"],
groupIds: [],
};
await expect(filterVisibleFileLinksWithTx(tx as never, "tenant_1", links, ctx)).resolves.toEqual([
links[0],
links[2],
links[4],
links[5],
links[6],
links[7],
links[8],
links[9],
]);
expect(tx.kontakt.findMany).toHaveBeenCalledTimes(2);
expect(tx.inboxLead.findMany).toHaveBeenCalledTimes(1);
expect(tx.tenantUser.findMany).toHaveBeenCalledTimes(1);
expect(tx.formSubmission.findMany).toHaveBeenCalledTimes(1);
expect(tx.task.findMany).toHaveBeenCalledTimes(1);
expect(tx.appointment.findMany).toHaveBeenCalledTimes(1);
expect(tx.groupUser.findMany).not.toHaveBeenCalled();
expect(tx.projekt.findMany).not.toHaveBeenCalled();
});
it("hydrates project principals once and checks project aliases plus signatures in one project batch", async () => {
const tx = transaction();
tx.userRole.findMany.mockResolvedValue([{ roleId: "role_1" }]);
tx.groupUser.findMany.mockResolvedValue([{ groupId: "group_1" }]);
tx.projektSignatureDocument.findMany.mockResolvedValue([
{ id: "signature_visible", projektId: "signature_project_visible" },
{ id: "signature_hidden", projektId: "signature_project_hidden" },
]);
tx.projekt.findMany.mockResolvedValue([{ id: "project_visible" }, { id: "deal_visible" }, { id: "signature_project_visible" }]);
const links = [
{ entityType: "signature_document", entityId: "signature_visible", linkRole: null },
{ entityType: "order", entityId: "order_hidden", linkRole: null },
{ entityType: "deal", entityId: "deal_visible", linkRole: null },
{ entityType: "signature_document", entityId: "signature_hidden", linkRole: null },
{ entityType: "project", entityId: "project_visible", linkRole: null },
];
const ctx: FileVisibilityContext = {
userId: "user_1",
permissions: ["projects.read_own_member", "signature.read"],
};
await expect(filterVisibleFileLinksWithTx(tx as never, "tenant_1", links, ctx)).resolves.toEqual([links[0], links[2], links[4]]);
expect(tx.userRole.findMany).toHaveBeenCalledTimes(1);
expect(tx.groupUser.findMany).toHaveBeenCalledTimes(1);
expect(tx.projektMitglied.findMany).not.toHaveBeenCalled();
expect(tx.projektSignatureDocument.findMany).toHaveBeenCalledTimes(1);
expect(tx.projekt.findMany).toHaveBeenCalledTimes(1);
expect(tx.projekt.findMany.mock.calls[0]?.[0]).not.toHaveProperty("take");
expect(tx.projekt.findMany.mock.calls[0]?.[0].where.id.in).toEqual([
"project_visible",
"deal_visible",
"order_hidden",
"signature_project_visible",
"signature_project_hidden",
]);
});
it("reuses project principals between file scope and detail-link filtering", async () => {
const tx = transaction();
tx.fileEntityLink.findMany.mockResolvedValue([{ entityId: "project_visible" }]);
tx.userRole.findMany.mockResolvedValue([{ roleId: "role_1" }]);
tx.groupUser.findMany.mockResolvedValue([{ groupId: "group_1" }]);
tx.projekt.findMany.mockResolvedValue([{ id: "project_visible" }]);
const ctx: FileVisibilityContext = {
userId: "user_1",
permissions: ["files.read_own", "projects.read_own_member"],
};
const queryState = createFileVisibilityQueryState();
await fileReadScopeWhereWithTx(tx as never, "tenant_1", ctx, ["file_1"], queryState);
await filterVisibleFileLinksWithTx(tx as never, "tenant_1", [{ entityType: "project", entityId: "project_visible", linkRole: null }], ctx, queryState);
expect(tx.userRole.findMany).toHaveBeenCalledTimes(1);
expect(tx.groupUser.findMany).toHaveBeenCalledTimes(1);
expect(tx.projektMitglied.findMany).not.toHaveBeenCalled();
expect(tx.projekt.findMany).toHaveBeenCalledTimes(2);
});
it("does not truncate a large set of links", async () => {
const tx = transaction();
const links = Array.from({ length: 600 }, (_, index) => ({
entityType: "contact",
entityId: `contact_${index}`,
linkRole: index % 2 === 0 ? "primary" : null,
}));
tx.kontakt.findMany.mockResolvedValue(links.map(({ entityId }) => ({ id: entityId })));
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["crm.read_all"] };
const result = await filterVisibleFileLinksWithTx(tx as never, "tenant_1", links, ctx);
expect(result).toEqual(links);
expect(tx.kontakt.findMany).toHaveBeenCalledTimes(1);
expect(tx.kontakt.findMany.mock.calls[0]?.[0]).not.toHaveProperty("take");
expect(tx.kontakt.findMany.mock.calls[0]?.[0].where.id.in).toHaveLength(600);
});
it("does not keep a reassigned contact visible to its creator", async () => {
const tx = transaction();
tx.kontakt.findMany.mockResolvedValue([]);
const link = { entityType: "contact", entityId: "contact_reassigned", linkRole: null };
const ctx: FileVisibilityContext = { userId: "former_creator", permissions: ["crm.read_own"] };
await expect(filterVisibleFileLinksWithTx(tx as never, "tenant_1", [link], ctx)).resolves.toEqual([]);
expect(tx.kontakt.findMany).toHaveBeenCalledWith({
where: {
id: { in: ["contact_reassigned"] },
tenantId: "tenant_1",
archivedAt: null,
kontaktTyp: "privatperson",
zustaendigerUserId: "former_creator",
},
select: { id: true },
});
});
});
describe("assertFileLinkTargetVisibleWithTx", () => {
const projectContext: FileVisibilityContext = {
userId: "user_1",
permissions: ["projects.read_own_member"],
roleIds: ["role_1"],
groupIds: ["group_1"],
};
it.each(["project", "deal", "order"] as const)("uses the central project and PipelineAccess scope for %s targets", async (entityType) => {
const tx = transaction();
tx.projekt.findFirst.mockResolvedValue({ id: "project_1" });
await expect(assertFileLinkTargetVisibleWithTx(tx as never, "tenant_1", entityType, "project_1", projectContext)).resolves.toBeUndefined();
expect(tx.projekt.findFirst).toHaveBeenCalledWith({
where: {
id: "project_1",
tenantId: "tenant_1",
archivedAt: null,
AND: [
{
OR: [
{
mitglieder: {
some: {
tenantId: "tenant_1",
archivedAt: null,
OR: [{ userId: "user_1" }, { groupId: { in: ["group_1"] } }],
},
},
},
],
},
{
pipeline: {
accessRules: {
none: {
tenantId: "tenant_1",
kannSehen: false,
OR: [
{ principalType: "user", principalId: "user_1" },
{ principalType: "role", principalId: "role_1" },
{ principalType: "group", principalId: "group_1" },
],
},
},
},
},
],
},
select: { id: true },
});
});
it.each([
["contact", "privatperson"],
["company", "organisation"],
] as const)("requires current CRM assignment and the correct discriminator for %s targets", async (entityType, kontaktTyp) => {
const tx = transaction();
tx.kontakt.findFirst.mockResolvedValue({ id: "contact_1" });
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["crm.read_own"] };
await expect(assertFileLinkTargetVisibleWithTx(tx as never, "tenant_1", entityType, "contact_1", ctx)).resolves.toBeUndefined();
expect(tx.kontakt.findFirst).toHaveBeenCalledWith({
where: {
id: "contact_1",
tenantId: "tenant_1",
archivedAt: null,
kontaktTyp,
zustaendigerUserId: "user_1",
},
select: { id: true },
});
});
it("enforces lead owner visibility", async () => {
const tx = transaction();
tx.inboxLead.findFirst.mockResolvedValue({ id: "lead_1" });
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["leads.read_own"] };
await expect(assertFileLinkTargetVisibleWithTx(tx as never, "tenant_1", "lead", "lead_1", ctx)).resolves.toBeUndefined();
expect(tx.inboxLead.findFirst).toHaveBeenCalledWith({
where: { id: "lead_1", tenantId: "tenant_1", archivedAt: null, zustaendigerUserId: "user_1" },
select: { id: true },
});
});
it("enforces task owner, watcher, assignee, and tenant-group visibility", async () => {
const tx = transaction();
tx.task.findFirst.mockResolvedValue({ id: "task_1" });
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["tasks.read_own"], groupIds: ["group_1"] };
await expect(assertFileLinkTargetVisibleWithTx(tx as never, "tenant_1", "task", "task_1", ctx)).resolves.toBeUndefined();
expect(tx.task.findFirst).toHaveBeenCalledWith({
where: {
id: "task_1",
tenantId: "tenant_1",
archivedAt: null,
OR: [
{ createdByUserId: "user_1" },
{ assignedToUserId: "user_1" },
{ watchers: { some: { tenantId: "tenant_1", userId: "user_1" } } },
{ assignedToGroupId: { in: ["group_1"] } },
],
},
select: { id: true },
});
});
it("allows linking to the caller's own active tenant membership", async () => {
const tx = transaction();
tx.tenantUser.findFirst.mockResolvedValue({ id: "membership_1" });
const ctx: FileVisibilityContext = { userId: "user_1", permissions: [] };
await expect(assertFileLinkTargetVisibleWithTx(tx as never, "tenant_1", "user", "user_1", ctx)).resolves.toBeUndefined();
expect(tx.tenantUser.findFirst).toHaveBeenCalledWith({
where: { tenantId: "tenant_1", userId: "user_1", status: TenantUserStatus.ACTIVE, user: { active: true } },
select: { id: true },
});
});
it("does not let member-directory access substitute for HR access to another user", async () => {
const tx = transaction();
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["app.settings.members.view"] };
await expect(assertFileLinkTargetVisibleWithTx(tx as never, "tenant_1", "user", "user_2", ctx)).rejects.toMatchObject({ status: 404 });
expect(tx.tenantUser.findFirst).not.toHaveBeenCalled();
});
it.each(["hr.read_all", "hr.admin"])("allows %s to link an active employee target", async (permission) => {
const tx = transaction();
tx.tenantUser.findFirst.mockResolvedValue({ id: "membership_2" });
const ctx: FileVisibilityContext = { userId: "user_1", permissions: [permission] };
await expect(assertFileLinkTargetVisibleWithTx(tx as never, "tenant_1", "user", "user_2", ctx)).resolves.toBeUndefined();
});
it("does not infer access to another employee from hr.write", async () => {
const tx = transaction();
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["hr.write"] };
await expect(assertFileLinkTargetVisibleWithTx(tx as never, "tenant_1", "user", "user_2", ctx)).rejects.toMatchObject({ status: 404 });
expect(tx.tenantUser.findFirst).not.toHaveBeenCalled();
});
it("scopes form submissions through a readable owned form", async () => {
const tx = transaction();
tx.formSubmission.findFirst.mockResolvedValue({ id: "submission_1" });
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["forms.basic"] };
await expect(assertFileLinkTargetVisibleWithTx(tx as never, "tenant_1", "form_submission", "submission_1", ctx)).resolves.toBeUndefined();
expect(tx.formSubmission.findFirst).toHaveBeenCalledWith({
where: { id: "submission_1", tenantId: "tenant_1", form: { tenantId: "tenant_1", createdByUserId: "user_1" } },
select: { id: true },
});
});
it("owner-scopes appointments unless the caller has tenant-wide booking read access", async () => {
const tx = transaction();
tx.appointment.findFirst.mockResolvedValue({ id: "appointment_1" });
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["bookings.read_own"] };
await expect(assertFileLinkTargetVisibleWithTx(tx as never, "tenant_1", "appointment", "appointment_1", ctx)).resolves.toBeUndefined();
expect(tx.appointment.findFirst).toHaveBeenCalledWith({
where: { id: "appointment_1", tenantId: "tenant_1", providerUserId: "user_1" },
select: { id: true },
});
});
it("requires both signature and project visibility for signature documents", async () => {
const tx = transaction();
tx.projektSignatureDocument.findFirst.mockResolvedValue({ id: "document_1", projektId: "project_1" });
tx.projekt.findFirst.mockResolvedValue({ id: "project_1" });
const ctx: FileVisibilityContext = {
userId: "user_1",
permissions: ["signature.read", "projects.read_all"],
roleIds: [],
groupIds: [],
};
await expect(assertFileLinkTargetVisibleWithTx(tx as never, "tenant_1", "signature_document", "document_1", ctx)).resolves.toBeUndefined();
expect(tx.projektSignatureDocument.findFirst).toHaveBeenCalledWith({
where: { id: "document_1", tenantId: "tenant_1", archivedAt: null },
select: { id: true, projektId: true },
});
expect(tx.projekt.findFirst.mock.calls[0]?.[0].where).toMatchObject({ id: "project_1", tenantId: "tenant_1", archivedAt: null });
});
it("fails closed before querying when the target permission is missing", async () => {
const tx = transaction();
const ctx: FileVisibilityContext = { userId: "user_1", permissions: [] };
await expect(assertFileLinkTargetVisibleWithTx(tx as never, "tenant_1", "contact", "contact_1", ctx)).rejects.toMatchObject({ status: 404 });
expect(tx.kontakt.findFirst).not.toHaveBeenCalled();
});
it("fails closed when the scoped target does not exist", async () => {
const tx = transaction();
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["bookings.read_all"] };
await expect(assertFileLinkTargetVisibleWithTx(tx as never, "tenant_1", "appointment", "foreign_appointment", ctx)).rejects.toMatchObject({
status: 404,
});
});
it("authorizes a chat-message target only through active channel membership, even for read-all admins", async () => {
const tx = transaction();
tx.teamChatMessage.findFirst.mockResolvedValue({ id: "message_1" });
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["chat.read_all_channels"] };
await expect(assertFileLinkTargetVisibleWithTx(tx as never, "tenant_1", "chat_message", "message_1", ctx)).resolves.toBeUndefined();
expect(tx.teamChatMessage.findFirst).toHaveBeenCalledWith({
where: {
id: "message_1",
tenantId: "tenant_1",
chat: { tenantId: "tenant_1", members: { some: { tenantId: "tenant_1", userId: "user_1", leftAt: null } } },
},
select: { id: true },
});
});
it("fails closed for a runtime entity type unsupported by the typed Files service", async () => {
const tx = transaction();
const ctx: FileVisibilityContext = { userId: "user_1", permissions: ["files.admin"] };
await expect(assertFileLinkTargetVisibleWithTx(tx as never, "tenant_1", "email_message" as FileEntityType, "message_1", ctx)).rejects.toMatchObject({
status: 404,
});
});
});