diff --git a/foundations/server/packages/middleware/src/guestCollaboratorClassRead.ts b/foundations/server/packages/middleware/src/guestCollaboratorClassRead.ts index 25df1d8c2c..69c9bf3b4a 100644 --- a/foundations/server/packages/middleware/src/guestCollaboratorClassRead.ts +++ b/foundations/server/packages/middleware/src/guestCollaboratorClassRead.ts @@ -42,11 +42,7 @@ function mergeDocIdRestriction (query: DocumentQuery, allowed: } type WithAnd = DocumentQuery & { $and?: DocumentQuery[] } const { _id: _drop, $and, ...rest } = query as WithAnd - const andParts: DocumentQuery[] = [ - ...($and ?? []), - { _id: prevId }, - { _id: allowedIds } - ] + const andParts: DocumentQuery[] = [...($and ?? []), { _id: prevId }, { _id: allowedIds }] // Spreading rest + $and is not inferred as DocumentQuery (mapped type + index signature). const merged: DocumentQuery = { ...rest, $and: andParts } return merged diff --git a/foundations/server/packages/middleware/src/tests/guestCollaboratorClassRead.test.ts b/foundations/server/packages/middleware/src/tests/guestCollaboratorClassRead.test.ts index 864cbfc128..5cca288452 100644 --- a/foundations/server/packages/middleware/src/tests/guestCollaboratorClassRead.test.ts +++ b/foundations/server/packages/middleware/src/tests/guestCollaboratorClassRead.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/consistent-type-assertions */ // // Copyright © 2026 Hardcore Engineering Inc. // @@ -29,10 +30,9 @@ import core, { type SearchResult, type SessionData, systemAccountUuid, - Timestamp, toFindResult } from '@hcengineering/core' -import type { Middleware, PipelineContext } from '@hcengineering/server-core' +import type { Middleware, PipelineContext, ServerFindOptions } from '@hcengineering/server-core' import { GuestCollaboratorClassReadMiddleware } from '../guestCollaboratorClassRead' const MEETING_MINUTES_CLASS = 'test:love:class:MeetingMinutes' as Ref> @@ -40,7 +40,7 @@ const DOC_CLASS = core.class.Doc function makeAccount (role: AccountRole, uuid?: ReturnType): Account { return { - uuid: (uuid ?? generateId()) as Account['uuid'], + uuid: (uuid ?? (generateId() as unknown as Account['uuid'])) as Account['uuid'], role, primarySocialId: 'test-social' as PersonId, socialIds: ['test-social' as PersonId], @@ -106,7 +106,7 @@ function makePipelineContext (): PipelineContext { adapterManager: {} as any, storageAdapter: {} as any, contextVars: {}, - lastTx: '' as Timestamp, + lastTx: '', lastHash: '', broadcastEvent: async () => {} } as PipelineContext @@ -127,18 +127,22 @@ function stubMiddleware (): Middleware { } describe('GuestCollaboratorClassReadMiddleware', () => { - const MM_ID = generateId() as Ref + const MM_ID = generateId() it('User: passes original query in a single findAll', async () => { - const captured: Array<{ cls: string; query: unknown }> = [] + const captured: Array<{ cls: string, query: unknown }> = [] const next: Middleware = { ...stubMiddleware(), - findAll: async (ctx, _class, query, options) => { + findAll: async (ctx: MeasureContext, _class: Ref>, query: any) => { captured.push({ cls: _class as string, query: { ...query } }) - return toFindResult([]) + return toFindResult([]) as any } } - const mw = new GuestCollaboratorClassReadMiddleware(makePipelineContext(), next) + const mw = await GuestCollaboratorClassReadMiddleware.create( + new MeasureMetricsContext('test', {}), + makePipelineContext(), + next + ) const ctx = makeCtx(makeAccount(AccountRole.User)) await mw.findAll(ctx, MEETING_MINUTES_CLASS, { attachedTo: MM_ID }) expect(captured).toHaveLength(1) @@ -149,18 +153,27 @@ describe('GuestCollaboratorClassReadMiddleware', () => { it('Guest: loads collaborators then restricts MeetingMinutes to collaborator attachedTo ids', async () => { const guest = makeAccount(AccountRole.Guest) const collabDoc = makeCollaboratorDoc(MM_ID, guest.uuid) - const captured: Array<{ cls: string; query: unknown }> = [] + const captured: Array<{ cls: string, query: unknown }> = [] const next: Middleware = { ...stubMiddleware(), - findAll: async (c, _class, query) => { + findAll: async ( + c: MeasureContext, + _class: Ref>, + query: any, + _options?: ServerFindOptions + ) => { captured.push({ cls: _class as string, query: JSON.parse(JSON.stringify(query)) }) - if (_class === core.class.Collaborator) { - return toFindResult([collabDoc]) + if (_class === (core.class.Collaborator as unknown as Ref>)) { + return toFindResult([collabDoc as any]) as any } - return toFindResult([]) + return toFindResult([]) as any } } - const mw = new GuestCollaboratorClassReadMiddleware(makePipelineContext(), next) + const mw = await GuestCollaboratorClassReadMiddleware.create( + new MeasureMetricsContext('test', {}), + makePipelineContext(), + next + ) const ctx = makeCtx(guest) await mw.findAll(ctx, MEETING_MINUTES_CLASS, { space: core.space.Workspace }) expect(captured).toHaveLength(2) @@ -172,18 +185,22 @@ describe('GuestCollaboratorClassReadMiddleware', () => { it('Guest: empty collaborator list yields _id $in []', async () => { const guest = makeAccount(AccountRole.Guest) - const captured: Array<{ cls: string; query: unknown }> = [] + const captured: Array<{ cls: string, query: unknown }> = [] const next: Middleware = { ...stubMiddleware(), - findAll: async (c, _class, query) => { + findAll: async (c: MeasureContext, _class: Ref>, query: any) => { captured.push({ cls: _class as string, query: JSON.parse(JSON.stringify(query)) }) - if (_class === core.class.Collaborator) { - return toFindResult([]) + if (_class === (core.class.Collaborator as unknown as Ref>)) { + return toFindResult([]) as any } - return toFindResult([]) + return toFindResult([]) as any } } - const mw = new GuestCollaboratorClassReadMiddleware(makePipelineContext(), next) + const mw = await GuestCollaboratorClassReadMiddleware.create( + new MeasureMetricsContext('test', {}), + makePipelineContext(), + next + ) const ctx = makeCtx(guest) await mw.findAll(ctx, MEETING_MINUTES_CLASS, {}) expect(captured).toHaveLength(2) @@ -196,18 +213,22 @@ describe('GuestCollaboratorClassReadMiddleware', () => { ...makePipelineContext(), modelDb: { findAllSync: () => [] - } as PipelineContext['modelDb'] + } as unknown as PipelineContext['modelDb'] } as PipelineContext const captured: unknown[] = [] const next: Middleware = { ...stubMiddleware(), - findAll: async (c, _class, query) => { + findAll: async (c: MeasureContext, _class: Ref>, query: any) => { captured.push(query) - return toFindResult([]) + return toFindResult([]) as any } } - const mw = new GuestCollaboratorClassReadMiddleware(bareContext, next) + const mw = await GuestCollaboratorClassReadMiddleware.create( + new MeasureMetricsContext('test', {}), + bareContext, + next + ) const ctx = makeCtx(makeAccount(AccountRole.Guest)) await mw.findAll(ctx, MEETING_MINUTES_CLASS, { space: core.space.Workspace }) expect(captured).toHaveLength(1) @@ -220,10 +241,14 @@ describe('GuestCollaboratorClassReadMiddleware', () => { ...stubMiddleware(), findAll: async () => { calls++ - return toFindResult([]) + return toFindResult([]) as any } } - const mw = new GuestCollaboratorClassReadMiddleware(makePipelineContext(), next) + const mw = await GuestCollaboratorClassReadMiddleware.create( + new MeasureMetricsContext('test', {}), + makePipelineContext(), + next + ) const ctx = makeCtx({ uuid: systemAccountUuid, role: AccountRole.Owner, @@ -237,26 +262,28 @@ describe('GuestCollaboratorClassReadMiddleware', () => { it('Guest: merges existing _id constraint with $and', async () => { const guest = makeAccount(AccountRole.Guest) - const otherId = generateId() as Ref + const otherId = generateId() const collabDoc = makeCollaboratorDoc(MM_ID, guest.uuid) const captured: unknown[] = [] const next: Middleware = { ...stubMiddleware(), - findAll: async (c, _class, query) => { + findAll: async (c: MeasureContext, _class: Ref>, query: any) => { captured.push(JSON.parse(JSON.stringify(query))) - if (_class === core.class.Collaborator) { - return toFindResult([collabDoc]) + if (_class === (core.class.Collaborator as unknown as Ref>)) { + return toFindResult([collabDoc as any]) as any } - return toFindResult([]) + return toFindResult([]) as any } } - const mw = new GuestCollaboratorClassReadMiddleware(makePipelineContext(), next) + const mw = await GuestCollaboratorClassReadMiddleware.create( + new MeasureMetricsContext('test', {}), + makePipelineContext(), + next + ) const ctx = makeCtx(guest) await mw.findAll(ctx, MEETING_MINUTES_CLASS, { _id: otherId }) const mmQuery = captured[1] as any expect(mmQuery.$and).toBeDefined() - expect(mmQuery.$and).toEqual( - expect.arrayContaining([{ _id: otherId }, { _id: { $in: [MM_ID] } }]) - ) + expect(mmQuery.$and).toEqual(expect.arrayContaining([{ _id: otherId }, { _id: { $in: [MM_ID] } }])) }) }) diff --git a/ws-tests/api-tests/src/__tests__/rest.test.ts b/ws-tests/api-tests/src/__tests__/rest.test.ts index a3dbcb3848..10958e6b73 100644 --- a/ws-tests/api-tests/src/__tests__/rest.test.ts +++ b/ws-tests/api-tests/src/__tests__/rest.test.ts @@ -350,51 +350,94 @@ describe('rest-api-server', () => { const guestEmail = 'guest1' const guestPassword = '1234' - it( - 'guest findAll does not return meeting minutes without collaborator', - async () => { - const userConn = connect() - const rooms = await userConn.findAll(loveClass.Room, {}, { limit: 1 }) - if (rooms.length === 0) { - throw new Error('No love Room in workspace — cannot seed MeetingMinutes') - } - const room = rooms[0] + it('guest findAll does not return meeting minutes without collaborator', async () => { + const userConn = connect() + const rooms = await userConn.findAll(loveClass.Room, {}, { limit: 1 }) + if (rooms.length === 0) { + throw new Error('No love Room in workspace — cannot seed MeetingMinutes') + } + const room = rooms[0] - const tx = await connectTx() - const title = `api-test-mm-${generateId()}` - const mmId = await tx.createDoc(loveClass.MeetingMinutes, core.space.Workspace, { - title, - description: null, - attachedTo: room._id, - attachedToClass: loveClass.Room, - collection: 'meetings', - status: LoveMeetingStatus.Finished - }) + const tx = await connectTx() + const title = `api-test-mm-${generateId()}` + const mmId = await tx.createDoc(loveClass.MeetingMinutes, core.space.Workspace, { + title, + description: null, + attachedTo: room._id, + attachedToClass: loveClass.Room, + collection: 'meetings', + status: LoveMeetingStatus.Finished + }) - try { - const userFound = await userConn.findAll(loveClass.MeetingMinutes, { _id: mmId }) - expect(userFound.length).toBe(1) + try { + const userFound = await userConn.findAll(loveClass.MeetingMinutes, { _id: mmId }) + expect(userFound.length).toBe(1) - const guestWs = await getWorkspaceToken( - 'http://huly.local:8083', - { - email: guestEmail, - password: guestPassword, - workspace: wsName - }, - serverConfig - ) - expect(guestWs.info.role).toBe(AccountRole.Guest) + const guestWs = await getWorkspaceToken( + 'http://huly.local:8083', + { + email: guestEmail, + password: guestPassword, + workspace: wsName + }, + serverConfig + ) + expect(guestWs.info.role).toBe(AccountRole.Guest) - const guestConn = createRestClient(guestWs.endpoint, guestWs.workspaceId, guestWs.token) - const guestFound = await guestConn.findAll(loveClass.MeetingMinutes, { _id: mmId }) - expect(guestFound.length).toBe(0) - } finally { - await tx.removeDoc(loveClass.MeetingMinutes, core.space.Workspace, mmId) - } - }, - 60000 - ) + const guestConn = createRestClient(guestWs.endpoint, guestWs.workspaceId, guestWs.token) + const guestFound = await guestConn.findAll(loveClass.MeetingMinutes, { _id: mmId }) + expect(guestFound.length).toBe(0) + } finally { + await tx.removeDoc(loveClass.MeetingMinutes, core.space.Workspace, mmId) + } + }, 60000) + + it('guest findAll returns meeting minutes when guest is a Collaborator', async () => { + const userConn = connect() + const rooms = await userConn.findAll(loveClass.Room, {}, { limit: 1 }) + if (rooms.length === 0) { + throw new Error('No love Room in workspace — cannot seed MeetingMinutes') + } + const room = rooms[0] + + const guestWs = await getWorkspaceToken( + 'http://huly.local:8083', + { + email: guestEmail, + password: guestPassword, + workspace: wsName + }, + serverConfig + ) + expect(guestWs.info.role).toBe(AccountRole.Guest) + + const tx = await connectTx() + const title = `api-test-mm-${generateId()}` + const mmId = await tx.createDoc(loveClass.MeetingMinutes, core.space.Workspace, { + title, + description: null, + attachedTo: room._id, + attachedToClass: loveClass.Room, + collection: 'meetings', + status: LoveMeetingStatus.Finished + }) + + const collabId = await tx.createDoc(core.class.Collaborator, core.space.Workspace, { + attachedTo: mmId, + attachedToClass: loveClass.MeetingMinutes, + collection: 'collaborators', + collaborator: guestWs.info.account + } as any) + + try { + const guestConn = createRestClient(guestWs.endpoint, guestWs.workspaceId, guestWs.token) + const guestFound = await guestConn.findAll(loveClass.MeetingMinutes, { _id: mmId }) + expect(guestFound.length).toBe(1) + } finally { + await tx.removeDoc(core.class.Collaborator, core.space.Workspace, collabId) + await tx.removeDoc(loveClass.MeetingMinutes, core.space.Workspace, mmId) + } + }, 60000) }) })