Allow viewers to request collaboration on posts
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
هذا الالتزام موجود في:
@@ -3338,13 +3338,29 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "formdata",
|
||||||
"raw": "{\n \"targetUserId\": \"{{targetUserId}}\",\n \"collaborationType\": \"{{collaborationType}}\",\n \"message\": \"{{collaborationMessage}}\",\n \"attachmentUrl\": \"{{collaborationAttachmentUrl}}\",\n \"attachmentType\": \"{{collaborationAttachmentType}}\"\n}",
|
"formdata": [
|
||||||
"options": {
|
{
|
||||||
"raw": {
|
"key": "collaborationType",
|
||||||
"language": "json"
|
"value": "{{collaborationType}}",
|
||||||
}
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "message",
|
||||||
|
"value": "حابب شارك بتعاون على هذا البوست",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "attachmentType",
|
||||||
|
"value": "{{collaborationAttachmentType}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "attachmentUrl",
|
||||||
|
"type": "file",
|
||||||
|
"src": []
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"event": [
|
"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",
|
"name": "List My Collaboration Requests",
|
||||||
"request": {
|
"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",
|
"name": "Smoke",
|
||||||
"item": [
|
"item": [
|
||||||
|
|||||||
@@ -2810,13 +2810,29 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "formdata",
|
||||||
"raw": "{\n \"targetUserId\": \"{{targetUserId}}\",\n \"collaborationType\": \"{{collaborationType}}\",\n \"message\": \"{{collaborationMessage}}\",\n \"attachmentUrl\": \"{{collaborationAttachmentUrl}}\",\n \"attachmentType\": \"{{collaborationAttachmentType}}\"\n}",
|
"formdata": [
|
||||||
"options": {
|
{
|
||||||
"raw": {
|
"key": "collaborationType",
|
||||||
"language": "json"
|
"value": "{{collaborationType}}",
|
||||||
}
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "message",
|
||||||
|
"value": "حابب شارك بتعاون على هذا البوست",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "attachmentType",
|
||||||
|
"value": "{{collaborationAttachmentType}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "attachmentUrl",
|
||||||
|
"type": "file",
|
||||||
|
"src": []
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"event": [
|
"event": [
|
||||||
@@ -2852,13 +2868,29 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "formdata",
|
||||||
"raw": "{\n \"collaborationType\": \"{{collaborationType}}\",\n \"message\": \"{{collaborationMessage}}\",\n \"attachmentUrl\": \"{{collaborationAttachmentUrl}}\",\n \"attachmentType\": \"{{collaborationAttachmentType}}\"\n}",
|
"formdata": [
|
||||||
"options": {
|
{
|
||||||
"raw": {
|
"key": "collaborationType",
|
||||||
"language": "json"
|
"value": "{{collaborationType}}",
|
||||||
}
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "message",
|
||||||
|
"value": "حابب نتعاون على عمل جديد",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "attachmentType",
|
||||||
|
"value": "{{collaborationAttachmentType}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "attachmentUrl",
|
||||||
|
"type": "file",
|
||||||
|
"src": []
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"event": [
|
"event": [
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const createService = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('CollaborationRequestsService', () => {
|
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 requesterId = new Types.ObjectId().toString();
|
||||||
const targetUserId = new Types.ObjectId().toString();
|
const targetUserId = new Types.ObjectId().toString();
|
||||||
const postId = new Types.ObjectId().toString();
|
const postId = new Types.ObjectId().toString();
|
||||||
@@ -81,7 +81,7 @@ describe('CollaborationRequestsService', () => {
|
|||||||
const { service, model, postsRepository, usersRepository, blocksRepository, notificationsService } =
|
const { service, model, postsRepository, usersRepository, blocksRepository, notificationsService } =
|
||||||
createService();
|
createService();
|
||||||
|
|
||||||
postsRepository.findById.mockResolvedValue({ authorId: new Types.ObjectId(requesterId) });
|
postsRepository.findById.mockResolvedValue({ authorId: new Types.ObjectId(targetUserId) });
|
||||||
usersRepository.findById.mockResolvedValue({ isDisabled: false });
|
usersRepository.findById.mockResolvedValue({ isDisabled: false });
|
||||||
blocksRepository.findAnyBetween.mockResolvedValue(null);
|
blocksRepository.findAnyBetween.mockResolvedValue(null);
|
||||||
model.findOne
|
model.findOne
|
||||||
@@ -91,7 +91,6 @@ describe('CollaborationRequestsService', () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
service.create(requesterId, postId, {
|
service.create(requesterId, postId, {
|
||||||
targetUserId,
|
|
||||||
collaborationType: 'duet',
|
collaborationType: 'duet',
|
||||||
message: 'Let us work',
|
message: 'Let us work',
|
||||||
}),
|
}),
|
||||||
@@ -100,6 +99,16 @@ describe('CollaborationRequestsService', () => {
|
|||||||
request: expect.objectContaining({ populated: true }),
|
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(notificationsService.create).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
actorId: requesterId,
|
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 () => {
|
it('creates a general collaboration request from a user profile', async () => {
|
||||||
const requesterId = new Types.ObjectId().toString();
|
const requesterId = new Types.ObjectId().toString();
|
||||||
const targetUserId = new Types.ObjectId().toString();
|
const targetUserId = new Types.ObjectId().toString();
|
||||||
@@ -272,7 +293,7 @@ describe('CollaborationRequestsService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(postsRepository.updateById).toHaveBeenCalledWith(postId.toString(), {
|
expect(postsRepository.updateById).toHaveBeenCalledWith(postId.toString(), {
|
||||||
$addToSet: { collaboratorIds: request.targetUserId },
|
$addToSet: { collaboratorIds: request.requesterId },
|
||||||
});
|
});
|
||||||
expect(notificationsService.create).toHaveBeenCalledWith(
|
expect(notificationsService.create).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|||||||
@@ -52,30 +52,27 @@ export class CollaborationRequestsService {
|
|||||||
dto: CreateCollaborationRequestDto,
|
dto: CreateCollaborationRequestDto,
|
||||||
file?: UploadedAudioFile,
|
file?: UploadedAudioFile,
|
||||||
) {
|
) {
|
||||||
const targetUserId = dto.targetUserId;
|
if (!Types.ObjectId.isValid(postId)) {
|
||||||
|
|
||||||
if (!Types.ObjectId.isValid(postId) || !Types.ObjectId.isValid(targetUserId)) {
|
|
||||||
throw new BadRequestException('Invalid collaboration request');
|
throw new BadRequestException('Invalid collaboration request');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requesterId === targetUserId) {
|
const post = await this.postsRepository.findById(postId);
|
||||||
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),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
throw new NotFoundException('Post not found');
|
throw new NotFoundException('Post not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (post.authorId.toString() !== requesterId) {
|
const targetUserId = post.authorId.toString();
|
||||||
throw new ForbiddenException('Only the post owner can invite collaborators');
|
|
||||||
|
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) {
|
if (!targetUser || targetUser.isDisabled) {
|
||||||
throw new NotFoundException('Target user not found');
|
throw new NotFoundException('Target user not found');
|
||||||
}
|
}
|
||||||
@@ -302,7 +299,7 @@ export class CollaborationRequestsService {
|
|||||||
|
|
||||||
if (request.postId) {
|
if (request.postId) {
|
||||||
await this.postsRepository.updateById(request.postId.toString(), {
|
await this.postsRepository.updateById(request.postId.toString(), {
|
||||||
$addToSet: { collaboratorIds: request.targetUserId },
|
$addToSet: { collaboratorIds: request.requesterId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import {
|
|||||||
} from '../schemas/collaboration-request.schema';
|
} from '../schemas/collaboration-request.schema';
|
||||||
|
|
||||||
export class CreateCollaborationRequestDto {
|
export class CreateCollaborationRequestDto {
|
||||||
|
@IsOptional()
|
||||||
@IsMongoId()
|
@IsMongoId()
|
||||||
targetUserId!: string;
|
targetUserId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(({ value }) => (typeof value === 'string' ? value.trim().toLowerCase() : value))
|
@Transform(({ value }) => (typeof value === 'string' ? value.trim().toLowerCase() : value))
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم