Files
outline/server/routes/api/accessRequests/accessRequests.test.ts
T
Tom Moor e9e565dc2b fix: Access request logic for collection managers (#12579)
* fix: Access request logic for collection managers

* test: Exercise collection-manager path in access request regression tests

Grant the non-workspace-admin manager a collection-level Admin membership
instead of a direct document-level membership, so authorization flows
through the collection-manager path being tested for #12567.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 08:24:51 -04:00

618 lines
18 KiB
TypeScript

import { CollectionPermission, DocumentPermission } from "@shared/types";
import { AccessRequest, UserMembership } from "@server/models";
import { AccessRequestStatus } from "@server/models/AccessRequest";
import {
buildAdmin,
buildCollection,
buildDocument,
buildTeam,
buildUser,
} from "@server/test/factories";
import { getTestServer } from "@server/test/support";
const server = getTestServer();
describe("#accessRequests.create", () => {
it("should require id", async () => {
const user = await buildUser();
const res = await server.post("/api/accessRequests.create", user);
const body = await res.json();
expect(res.status).toEqual(400);
expect(body.message).toEqual("documentId: Must be a valid UUID or slug");
});
it("should require authentication", async () => {
const document = await buildDocument();
const res = await server.post("/api/accessRequests.create", {
body: {
documentId: document.id,
},
});
expect(res.status).toEqual(401);
});
it("should return 404 for non-existent document", async () => {
const user = await buildUser();
const res = await server.post("/api/accessRequests.create", user, {
body: {
documentId: "a8f22c38-f4eb-4909-8c30-b927af36c5f3",
},
});
const body = await res.json();
expect(res.status).toEqual(404);
expect(body.message).toEqual("Resource not found");
});
it("should create access request for a document", async () => {
const team = await buildTeam();
const owner = await buildUser({ teamId: team.id });
const requester = await buildUser({ teamId: team.id });
const collection = await buildCollection({
teamId: team.id,
userId: owner.id,
permission: null,
});
const document = await buildDocument({
teamId: team.id,
createdById: owner.id,
collectionId: collection.id,
});
const res = await server.post("/api/accessRequests.create", requester, {
body: {
documentId: document.id,
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.documentId).toEqual(document.id);
expect(body.data.userId).toEqual(requester.id);
});
it("should reject if user already has access", async () => {
const team = await buildTeam();
const owner = await buildUser({ teamId: team.id });
const requester = await buildUser({ teamId: team.id });
const document = await buildDocument({
teamId: team.id,
createdById: owner.id,
});
const res = await server.post("/api/accessRequests.create", requester, {
body: {
documentId: document.id,
},
});
expect(res.status).toEqual(400);
});
it("should work with document urlId", async () => {
const team = await buildTeam();
const owner = await buildUser({ teamId: team.id });
const requester = await buildUser({ teamId: team.id });
const collection = await buildCollection({
teamId: team.id,
userId: owner.id,
permission: null,
});
const document = await buildDocument({
teamId: team.id,
createdById: owner.id,
collectionId: collection.id,
});
const res = await server.post("/api/accessRequests.create", requester, {
body: {
documentId: document.urlId,
},
});
expect(res.status).toEqual(200);
});
it("should not allow new request if pending exists", async () => {
const team = await buildTeam();
const requester = await buildUser({ teamId: team.id });
const admin = await buildAdmin({ teamId: team.id });
const collection = await buildCollection({
teamId: team.id,
userId: admin.id,
permission: null,
});
const document = await buildDocument({
createdById: admin.id,
teamId: team.id,
collectionId: collection.id,
});
// Create first access request
const res1 = await server.post("/api/accessRequests.create", requester, {
body: {
documentId: document.id,
},
});
// Try to create another
const res2 = await server.post("/api/accessRequests.create", requester, {
body: {
documentId: document.id,
},
});
expect(res1.status).toEqual(200);
expect(res2.status).toEqual(400);
// Verify only one access request exists
const count = await AccessRequest.count({
where: {
documentId: document.id,
userId: requester.id,
},
});
expect(count).toEqual(1);
});
it("should allow creating new request after previous was dismissed", async () => {
const team = await buildTeam();
const requester = await buildUser({ teamId: team.id });
const admin = await buildAdmin({ teamId: team.id });
const collection = await buildCollection({
teamId: team.id,
userId: admin.id,
permission: null,
});
const document = await buildDocument({
createdById: admin.id,
teamId: team.id,
collectionId: collection.id,
});
// Create and dismiss first request
const res1 = await AccessRequest.create({
documentId: document.id,
userId: requester.id,
teamId: team.id,
status: AccessRequestStatus.Dismissed,
responderId: admin.id,
respondedAt: new Date(),
});
// Create new request
const res2 = await server.post("/api/accessRequests.create", requester, {
body: {
documentId: document.id,
},
});
const body = await res2.json();
expect(res2.status).toEqual(200);
expect(body.data.id).not.toEqual(res1.id);
expect(body.data.status).toEqual(AccessRequestStatus.Pending);
});
});
describe("#accessRequests.info", () => {
it("should require authentication", async () => {
const res = await server.post("/api/accessRequests.info");
expect(res.status).toEqual(401);
});
it("should fail if both id and documentId are missing", async () => {
const user = await buildUser();
const res = await server.post("/api/accessRequests.info", user);
expect(res.status).toEqual(400);
});
it("should return access request correctly by id", async () => {
const team = await buildTeam();
const requester = await buildUser({ teamId: team.id });
const admin = await buildAdmin({ teamId: team.id });
const document = await buildDocument({
createdById: admin.id,
teamId: team.id,
});
const accessRequest = await AccessRequest.create({
documentId: document.id,
userId: requester.id,
teamId: team.id,
});
const res = await server.post("/api/accessRequests.info", requester, {
body: {
id: accessRequest.id,
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.id).toEqual(accessRequest.id);
expect(body.data.status).toEqual(AccessRequestStatus.Pending);
});
it("should return access request correctly by documentId", async () => {
const team = await buildTeam();
const requester = await buildUser({ teamId: team.id });
const admin = await buildAdmin({ teamId: team.id });
const document = await buildDocument({
createdById: admin.id,
teamId: team.id,
});
const accessRequest = await AccessRequest.create({
documentId: document.id,
userId: requester.id,
teamId: team.id,
});
const res = await server.post("/api/accessRequests.info", requester, {
body: {
documentId: document.urlId,
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.id).toEqual(accessRequest.id);
expect(body.data.status).toEqual(AccessRequestStatus.Pending);
});
it("should return 404 if access request not found", async () => {
const user = await buildUser();
const res = await server.post("/api/accessRequests.info", user, {
body: {
id: "00000000-0000-0000-0000-000000000000",
},
});
expect(res.status).toEqual(404);
});
});
describe("#accessRequests.approve", () => {
it("should require authentication", async () => {
const res = await server.post("/api/accessRequests.approve");
expect(res.status).toEqual(401);
});
it("should approve an access request and grant access", async () => {
const team = await buildTeam();
const requester = await buildUser({ teamId: team.id });
const admin = await buildAdmin({ teamId: team.id });
const document = await buildDocument({
createdById: admin.id,
teamId: team.id,
});
const accessRequest = await AccessRequest.create({
documentId: document.id,
userId: requester.id,
teamId: team.id,
status: AccessRequestStatus.Pending,
});
const res = await server.post("/api/accessRequests.approve", admin, {
body: {
id: accessRequest.id,
permission: DocumentPermission.ReadWrite,
},
});
const body = await res.json();
expect(body.data.status).toEqual(AccessRequestStatus.Approved);
expect(body.data.responderId).toEqual(admin.id);
// // Verify that the user now has access
const membership = await UserMembership.findOne({
where: {
userId: requester.id,
documentId: document.id,
},
});
expect(membership).toBeTruthy();
expect(membership?.permission).toEqual(DocumentPermission.ReadWrite);
});
it("should allow a document manager who is not a workspace admin to approve", async () => {
const team = await buildTeam();
const requester = await buildUser({ teamId: team.id });
const manager = await buildUser({ teamId: team.id });
const collection = await buildCollection({
teamId: team.id,
userId: manager.id,
permission: null,
});
const document = await buildDocument({
teamId: team.id,
createdById: manager.id,
collectionId: collection.id,
});
await UserMembership.create({
userId: manager.id,
collectionId: collection.id,
createdById: manager.id,
permission: CollectionPermission.Admin,
});
const accessRequest = await AccessRequest.create({
documentId: document.id,
userId: requester.id,
teamId: team.id,
status: AccessRequestStatus.Pending,
});
const res = await server.post("/api/accessRequests.approve", manager, {
body: {
id: accessRequest.id,
permission: DocumentPermission.Read,
},
});
expect(res.status).toEqual(200);
const membership = await UserMembership.findOne({
where: {
userId: requester.id,
documentId: document.id,
},
});
expect(membership).toBeTruthy();
expect(membership?.permission).toEqual(DocumentPermission.Read);
});
it("should not allow non-managers to approve requests", async () => {
const team = await buildTeam();
const requester = await buildUser({ teamId: team.id });
const admin = await buildAdmin({ teamId: team.id });
const nonManager = await buildUser();
const document = await buildDocument({
createdById: admin.id,
teamId: team.id,
});
await UserMembership.create({
userId: admin.id,
documentId: document.id,
createdById: admin.id,
permission: DocumentPermission.ReadWrite,
});
const accessRequest = await AccessRequest.create({
documentId: document.id,
userId: requester.id,
teamId: team.id,
});
const res = await server.post("/api/accessRequests.approve", nonManager, {
body: {
id: accessRequest.id,
permission: DocumentPermission.ReadWrite,
},
});
expect(res.status).toEqual(403);
});
it("should be a no-op when user already has membership", async () => {
const team = await buildTeam();
const requester = await buildUser({ teamId: team.id });
const admin = await buildAdmin({ teamId: team.id });
const document = await buildDocument({
createdById: admin.id,
teamId: team.id,
});
await UserMembership.create({
userId: requester.id,
documentId: document.id,
createdById: admin.id,
permission: DocumentPermission.Read,
});
const accessRequest = await AccessRequest.create({
documentId: document.id,
userId: requester.id,
teamId: team.id,
status: AccessRequestStatus.Pending,
});
const res = await server.post("/api/accessRequests.approve", admin, {
body: {
id: accessRequest.id,
permission: DocumentPermission.ReadWrite,
},
});
expect(res.status).toEqual(200);
// Existing membership is unchanged.
const memberships = await UserMembership.findAll({
where: {
userId: requester.id,
documentId: document.id,
},
});
expect(memberships).toHaveLength(1);
expect(memberships[0].permission).toEqual(DocumentPermission.Read);
});
it("should not allow approving requests that have been responded to", async () => {
const team = await buildTeam();
const requester = await buildUser({ teamId: team.id });
const admin = await buildAdmin({ teamId: team.id });
const document = await buildDocument({
createdById: admin.id,
teamId: team.id,
});
// Create access request that's already approved
const accessRequest = await AccessRequest.create({
documentId: document.id,
userId: requester.id,
teamId: team.id,
status: AccessRequestStatus.Approved,
responderId: admin.id,
respondedAt: new Date(),
});
const res = await server.post("/api/accessRequests.approve", admin, {
body: {
id: accessRequest.id,
permission: DocumentPermission.ReadWrite,
},
});
expect(res.status).toEqual(400);
});
});
describe("#accessRequests.dismiss", () => {
it("should require authentication", async () => {
const res = await server.post("/api/accessRequests.dismiss");
expect(res.status).toEqual(401);
});
it("should dismiss an access request", async () => {
const team = await buildTeam();
const requester = await buildUser({ teamId: team.id });
const admin = await buildAdmin({ teamId: team.id });
const document = await buildDocument({
createdById: admin.id,
teamId: team.id,
});
// Create access request
const accessRequest = await AccessRequest.create({
documentId: document.id,
userId: requester.id,
teamId: team.id,
});
const res = await server.post("/api/accessRequests.dismiss", admin, {
body: {
id: accessRequest.id,
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.status).toEqual(AccessRequestStatus.Dismissed);
expect(body.data.responderId).toEqual(admin.id);
const membership = await UserMembership.findOne({
where: {
userId: requester.id,
documentId: document.id,
},
});
expect(membership).toBeNull();
});
it("should allow a document manager who is not a workspace admin to dismiss", async () => {
const team = await buildTeam();
const requester = await buildUser({ teamId: team.id });
const manager = await buildUser({ teamId: team.id });
const collection = await buildCollection({
teamId: team.id,
userId: manager.id,
permission: null,
});
const document = await buildDocument({
teamId: team.id,
createdById: manager.id,
collectionId: collection.id,
});
await UserMembership.create({
userId: manager.id,
collectionId: collection.id,
createdById: manager.id,
permission: CollectionPermission.Admin,
});
const accessRequest = await AccessRequest.create({
documentId: document.id,
userId: requester.id,
teamId: team.id,
});
const res = await server.post("/api/accessRequests.dismiss", manager, {
body: {
id: accessRequest.id,
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.status).toEqual(AccessRequestStatus.Dismissed);
expect(body.data.responderId).toEqual(manager.id);
});
it("should not allow non-managers to dismiss requests", async () => {
const team = await buildTeam();
const requester = await buildUser({ teamId: team.id });
const admin = await buildAdmin({ teamId: team.id });
const nonManager = await buildUser();
const document = await buildDocument({
userId: admin.id,
teamId: team.id,
});
// add non-manager to the document with editor access
await UserMembership.create({
userId: admin.id,
documentId: document.id,
createdById: admin.id,
permission: DocumentPermission.ReadWrite,
});
// Create access request
const accessRequest = await AccessRequest.create({
documentId: document.id,
userId: requester.id,
teamId: team.id,
});
const res = await server.post("/api/accessRequests.dismiss", nonManager, {
body: {
id: accessRequest.id,
},
});
expect(res.status).toEqual(403);
});
it("should be a no-op when request has already been responded to", async () => {
const team = await buildTeam();
const requester = await buildUser({ teamId: team.id });
const admin = await buildAdmin({ teamId: team.id });
const responder = await buildAdmin({ teamId: team.id });
const document = await buildDocument({
createdById: admin.id,
teamId: team.id,
});
const respondedAt = new Date();
const accessRequest = await AccessRequest.create({
documentId: document.id,
userId: requester.id,
teamId: team.id,
status: AccessRequestStatus.Dismissed,
responderId: responder.id,
respondedAt,
});
const res = await server.post("/api/accessRequests.dismiss", admin, {
body: {
id: accessRequest.id,
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.status).toEqual(AccessRequestStatus.Dismissed);
expect(body.data.responderId).toEqual(responder.id);
});
});