diff --git a/postman/Oudelaa-Auth-Users-Posts.postman_collection.json b/postman/Oudelaa-Auth-Users-Posts.postman_collection.json index 522cde8..a517558 100644 --- a/postman/Oudelaa-Auth-Users-Posts.postman_collection.json +++ b/postman/Oudelaa-Auth-Users-Posts.postman_collection.json @@ -3338,13 +3338,29 @@ } ], "body": { - "mode": "raw", - "raw": "{\n \"targetUserId\": \"{{targetUserId}}\",\n \"collaborationType\": \"{{collaborationType}}\",\n \"message\": \"{{collaborationMessage}}\",\n \"attachmentUrl\": \"{{collaborationAttachmentUrl}}\",\n \"attachmentType\": \"{{collaborationAttachmentType}}\"\n}", - "options": { - "raw": { - "language": "json" + "mode": "formdata", + "formdata": [ + { + "key": "collaborationType", + "value": "{{collaborationType}}", + "type": "text" + }, + { + "key": "message", + "value": "حابب شارك بتعاون على هذا البوست", + "type": "text" + }, + { + "key": "attachmentType", + "value": "{{collaborationAttachmentType}}", + "type": "text" + }, + { + "key": "attachmentUrl", + "type": "file", + "src": [] } - } + ] } }, "event": [ @@ -3368,6 +3384,62 @@ } ] }, + { + "name": "Create General Collaboration Request", + "request": { + "method": "POST", + "url": "{{baseUrl}}/users/{{targetUserId}}/collaboration-requests", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "collaborationType", + "value": "{{collaborationType}}", + "type": "text" + }, + { + "key": "message", + "value": "حابب نتعاون على عمل جديد", + "type": "text" + }, + { + "key": "attachmentType", + "value": "{{collaborationAttachmentType}}", + "type": "text" + }, + { + "key": "attachmentUrl", + "type": "file", + "src": [] + } + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200 or 201', function () { pm.expect([200, 201]).to.include(pm.response.code); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property('request');", + "pm.environment.set('collaborationRequestId', json.request._id || json.request.id);", + "pm.test('General collaboration request has no post', function () {", + " pm.expect(json.request.postId === null || typeof json.request.postId === 'undefined').to.equal(true);", + " pm.expect(json.request).to.have.property('status', 'pending');", + "});" + ] + } + } + ] + }, { "name": "List My Collaboration Requests", "request": { @@ -6183,6 +6255,117 @@ } ] }, + { + "name": "Music World", + "item": [ + { + "name": "Get Music World Config", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/music-world", + "description": "Arabic-friendly static screen metadata and optional shortcut cards. The main Instagram-like grid should use Music World Explore Grid." + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property('title', '???? ????????');", + "pm.expect(json).to.have.property('searchPlaceholder');", + "pm.expect(json.cards).to.be.an('array');", + "pm.expect(json.sections).to.be.an('array');" + ] + } + } + ] + }, + { + "name": "Music World Explore Grid", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/music-world/explore?page=1&limit=20&cursor={{feedCursor}}", + "description": "Main Flutter Music World grid. Uses real feed explore posts and maps them to displayUrl/thumbnailUrl for Instagram-like explore UI." + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an('array');", + "pm.expect(json).to.have.property('pagination');", + "pm.expect(json.pagination).to.have.property('hasNextPage');", + "if ((json.items || []).length) {", + " const item = json.items[0];", + " pm.expect(item).to.have.property('id');", + " pm.expect(item).to.have.property('postType');", + " pm.expect(item).to.have.property('thumbnailUrl');", + " pm.expect(item).to.have.property('displayUrl');", + " pm.expect(item).to.have.property('media');", + " pm.expect(item).to.have.property('author');", + " pm.expect(item).to.have.property('engagement');", + " pm.expect(item).to.have.property('isLiked');", + " pm.expect(item).to.have.property('isSaved');", + "}" + ] + } + } + ] + }, + { + "name": "Music World Search Posts", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/music-world/search?q={{searchQuery}}&page=1&limit={{searchLimit}}", + "description": "Search bar endpoint for Music World. Searches real posts/media, not static categories." + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an('array');", + "pm.expect(json).to.have.property('pagination');", + "if ((json.items || []).length) {", + " const item = json.items[0];", + " pm.expect(item).to.have.property('id');", + " pm.expect(item).to.have.property('postType');", + " pm.expect(item).to.have.property('displayUrl');", + " pm.expect(item).to.have.property('author');", + "}" + ] + } + } + ] + } + ] + }, { "name": "Smoke", "item": [ @@ -10226,4 +10409,4 @@ "value": "{{accessToken}}" } ] -} \ No newline at end of file +} diff --git a/postman/Oudelaa-Mobile.postman_collection.json b/postman/Oudelaa-Mobile.postman_collection.json index 07a41e4..f7fa731 100644 --- a/postman/Oudelaa-Mobile.postman_collection.json +++ b/postman/Oudelaa-Mobile.postman_collection.json @@ -2810,13 +2810,29 @@ } ], "body": { - "mode": "raw", - "raw": "{\n \"targetUserId\": \"{{targetUserId}}\",\n \"collaborationType\": \"{{collaborationType}}\",\n \"message\": \"{{collaborationMessage}}\",\n \"attachmentUrl\": \"{{collaborationAttachmentUrl}}\",\n \"attachmentType\": \"{{collaborationAttachmentType}}\"\n}", - "options": { - "raw": { - "language": "json" + "mode": "formdata", + "formdata": [ + { + "key": "collaborationType", + "value": "{{collaborationType}}", + "type": "text" + }, + { + "key": "message", + "value": "حابب شارك بتعاون على هذا البوست", + "type": "text" + }, + { + "key": "attachmentType", + "value": "{{collaborationAttachmentType}}", + "type": "text" + }, + { + "key": "attachmentUrl", + "type": "file", + "src": [] } - } + ] } }, "event": [ @@ -2852,13 +2868,29 @@ } ], "body": { - "mode": "raw", - "raw": "{\n \"collaborationType\": \"{{collaborationType}}\",\n \"message\": \"{{collaborationMessage}}\",\n \"attachmentUrl\": \"{{collaborationAttachmentUrl}}\",\n \"attachmentType\": \"{{collaborationAttachmentType}}\"\n}", - "options": { - "raw": { - "language": "json" + "mode": "formdata", + "formdata": [ + { + "key": "collaborationType", + "value": "{{collaborationType}}", + "type": "text" + }, + { + "key": "message", + "value": "حابب نتعاون على عمل جديد", + "type": "text" + }, + { + "key": "attachmentType", + "value": "{{collaborationAttachmentType}}", + "type": "text" + }, + { + "key": "attachmentUrl", + "type": "file", + "src": [] } - } + ] } }, "event": [ diff --git a/src/modules/collaboration-requests/collaboration-requests.service.spec.ts b/src/modules/collaboration-requests/collaboration-requests.service.spec.ts index 36daa6e..b591d9e 100644 --- a/src/modules/collaboration-requests/collaboration-requests.service.spec.ts +++ b/src/modules/collaboration-requests/collaboration-requests.service.spec.ts @@ -69,7 +69,7 @@ const createService = () => { }; describe('CollaborationRequestsService', () => { - it('creates a pending collaboration request and notifies the target user', async () => { + it('allows a non-owner viewer to request collaboration on a post and notifies the post owner', async () => { const requesterId = new Types.ObjectId().toString(); const targetUserId = new Types.ObjectId().toString(); const postId = new Types.ObjectId().toString(); @@ -81,7 +81,7 @@ describe('CollaborationRequestsService', () => { const { service, model, postsRepository, usersRepository, blocksRepository, notificationsService } = createService(); - postsRepository.findById.mockResolvedValue({ authorId: new Types.ObjectId(requesterId) }); + postsRepository.findById.mockResolvedValue({ authorId: new Types.ObjectId(targetUserId) }); usersRepository.findById.mockResolvedValue({ isDisabled: false }); blocksRepository.findAnyBetween.mockResolvedValue(null); model.findOne @@ -91,7 +91,6 @@ describe('CollaborationRequestsService', () => { await expect( service.create(requesterId, postId, { - targetUserId, collaborationType: 'duet', message: 'Let us work', }), @@ -100,6 +99,16 @@ describe('CollaborationRequestsService', () => { request: expect.objectContaining({ populated: true }), }); + expect(model.findOneAndUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + postId: new Types.ObjectId(postId), + requesterId: new Types.ObjectId(requesterId), + targetUserId: new Types.ObjectId(targetUserId), + status: 'pending', + }), + expect.any(Object), + expect.any(Object), + ); expect(notificationsService.create).toHaveBeenCalledWith( expect.objectContaining({ actorId: requesterId, @@ -111,6 +120,18 @@ describe('CollaborationRequestsService', () => { ); }); + it('prevents the post owner from requesting collaboration on their own post', async () => { + const requesterId = new Types.ObjectId().toString(); + const postId = new Types.ObjectId().toString(); + const { service, postsRepository } = createService(); + + postsRepository.findById.mockResolvedValue({ authorId: new Types.ObjectId(requesterId) }); + + await expect(service.create(requesterId, postId, {})).rejects.toThrow( + 'You cannot request collaboration on your own post', + ); + }); + it('creates a general collaboration request from a user profile', async () => { const requesterId = new Types.ObjectId().toString(); const targetUserId = new Types.ObjectId().toString(); @@ -272,7 +293,7 @@ describe('CollaborationRequestsService', () => { }); expect(postsRepository.updateById).toHaveBeenCalledWith(postId.toString(), { - $addToSet: { collaboratorIds: request.targetUserId }, + $addToSet: { collaboratorIds: request.requesterId }, }); expect(notificationsService.create).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/src/modules/collaboration-requests/collaboration-requests.service.ts b/src/modules/collaboration-requests/collaboration-requests.service.ts index df0bf7d..cfe1283 100644 --- a/src/modules/collaboration-requests/collaboration-requests.service.ts +++ b/src/modules/collaboration-requests/collaboration-requests.service.ts @@ -52,30 +52,27 @@ export class CollaborationRequestsService { dto: CreateCollaborationRequestDto, file?: UploadedAudioFile, ) { - const targetUserId = dto.targetUserId; - - if (!Types.ObjectId.isValid(postId) || !Types.ObjectId.isValid(targetUserId)) { + if (!Types.ObjectId.isValid(postId)) { throw new BadRequestException('Invalid collaboration request'); } - if (requesterId === targetUserId) { - throw new BadRequestException('You cannot invite yourself'); - } - - const [post, targetUser, block] = await Promise.all([ - this.postsRepository.findById(postId), - this.usersRepository.findById(targetUserId), - this.blocksRepository.findAnyBetween(requesterId, targetUserId), - ]); + const post = await this.postsRepository.findById(postId); if (!post) { throw new NotFoundException('Post not found'); } - if (post.authorId.toString() !== requesterId) { - throw new ForbiddenException('Only the post owner can invite collaborators'); + const targetUserId = post.authorId.toString(); + + if (requesterId === targetUserId) { + throw new BadRequestException('You cannot request collaboration on your own post'); } + const [targetUser, block] = await Promise.all([ + this.usersRepository.findById(targetUserId), + this.blocksRepository.findAnyBetween(requesterId, targetUserId), + ]); + if (!targetUser || targetUser.isDisabled) { throw new NotFoundException('Target user not found'); } @@ -302,7 +299,7 @@ export class CollaborationRequestsService { if (request.postId) { await this.postsRepository.updateById(request.postId.toString(), { - $addToSet: { collaboratorIds: request.targetUserId }, + $addToSet: { collaboratorIds: request.requesterId }, }); } @@ -568,4 +565,4 @@ export class CollaborationRequestsService { deepLink: `/users/${actorId}`, }; } -} \ No newline at end of file +} diff --git a/src/modules/collaboration-requests/dto/create-collaboration-request.dto.ts b/src/modules/collaboration-requests/dto/create-collaboration-request.dto.ts index a294d2a..b754847 100644 --- a/src/modules/collaboration-requests/dto/create-collaboration-request.dto.ts +++ b/src/modules/collaboration-requests/dto/create-collaboration-request.dto.ts @@ -8,8 +8,9 @@ import { } from '../schemas/collaboration-request.schema'; export class CreateCollaborationRequestDto { + @IsOptional() @IsMongoId() - targetUserId!: string; + targetUserId?: string; @IsOptional() @Transform(({ value }) => (typeof value === 'string' ? value.trim().toLowerCase() : value))