diff --git a/oudelaa_dashboard/lib/api/endpoints.ts b/oudelaa_dashboard/lib/api/endpoints.ts index f45e234..c133d38 100644 --- a/oudelaa_dashboard/lib/api/endpoints.ts +++ b/oudelaa_dashboard/lib/api/endpoints.ts @@ -13,6 +13,28 @@ export const apiEndpoints = { revokeSuperAdminSession: (sessionId: string) => `/auth/superadmin/sessions/${sessionId}/revoke`, }, users: { + meProfileSetup: "/users/me/profile-setup", + meMusicSetup: "/users/me/music-setup", + me: "/users/me", + publicList: "/users", + publicById: (userId: string) => `/users/${userId}`, + publicDiscover: (params: Record = {}) => + `/users/discover?${toQueryString(params)}`, + publicProfileOverview: (userId: string) => `/users/${userId}/profile-overview`, + publicPresence: (userId: string) => `/users/${userId}/presence`, + publicPosts: (userId: string, params: Record = {}) => + `/users/${userId}/posts?${toQueryString(params)}`, + follow: (userId: string) => `/users/${userId}/follow`, + unfollow: (userId: string) => `/users/${userId}/follow`, + followStatus: (userId: string) => `/users/${userId}/follow-status`, + followers: (userId: string, params: Record = {}) => + `/users/${userId}/followers?${toQueryString(params)}`, + following: (userId: string, params: Record = {}) => + `/users/${userId}/following?${toQueryString(params)}`, + myFollowers: (params: Record = {}) => + `/users/me/followers?${toQueryString(params)}`, + myFollowing: (params: Record = {}) => + `/users/me/following?${toQueryString(params)}`, all: (params: Record = {}) => `/users/admin?${toQueryString(params)}`, byId: (userId: string) => `/users/admin/${userId}`, @@ -31,6 +53,24 @@ export const apiEndpoints = { profileOverview: (userId: string) => `/users/admin/${userId}/profile-overview`, }, posts: { + create: "/posts", + reelsCreate: "/posts/reels", + reels: (params: Record = {}) => + `/posts/reels?${toQueryString(params)}`, + byId: (postId: string) => `/posts/${postId}`, + byUser: (userId: string, params: Record = {}) => + `/posts/user/${userId}?${toQueryString(params)}`, + update: (postId: string) => `/posts/${postId}`, + repost: (postId: string) => `/posts/${postId}/repost`, + commentSettings: (postId: string) => `/posts/${postId}/comment-settings`, + pinProfile: (postId: string) => `/posts/${postId}/pin-profile`, + unpinProfile: (postId: string) => `/posts/${postId}/unpin-profile`, + archive: (postId: string) => `/posts/${postId}/archive`, + restoreArchive: (postId: string) => `/posts/${postId}/restore-archive`, + remove: (postId: string) => `/posts/${postId}`, + view: (postId: string) => `/posts/${postId}/view`, + play: (postId: string) => `/posts/${postId}/play`, + share: (postId: string) => `/posts/${postId}/share`, moderation: (params: Record = {}) => `/posts/admin/moderation?${toQueryString(params)}`, adminDelete: (postId: string) => `/posts/admin/${postId}`, @@ -41,9 +81,39 @@ export const apiEndpoints = { adminDelete: (commentId: string) => `/comments/admin/${commentId}`, }, notifications: { + mine: (params: Record = {}) => + `/notifications?${toQueryString(params)}`, + unreadCount: "/notifications/unread-count", + markRead: (notificationId: string) => `/notifications/${notificationId}/read`, + markAllRead: "/notifications/read-all", superAdmin: (params: Record = {}) => `/notifications/superadmin?${toQueryString(params)}`, }, + follows: { + toggle: "/follows/toggle", + followers: (userId: string, params: Record = {}) => + `/follows/followers/${userId}?${toQueryString(params)}`, + following: (userId: string, params: Record = {}) => + `/follows/following/${userId}?${toQueryString(params)}`, + status: (targetUserId: string) => `/follows/status/${targetUserId}`, + suggestions: (params: Record = {}) => + `/follows/suggestions?${toQueryString(params)}`, + requests: (params: Record = {}) => + `/follows/requests?${toQueryString(params)}`, + approveRequest: (requestId: string) => `/follows/requests/${requestId}/approve`, + rejectRequest: (requestId: string) => `/follows/requests/${requestId}/reject`, + }, + devices: { + register: "/devices/register", + unregister: "/devices/unregister", + }, + collaborationRequests: { + create: (postId: string) => `/posts/${postId}/collaboration-requests`, + list: (params: Record = {}) => + `/collaboration-requests?${toQueryString(params)}`, + approve: (requestId: string) => `/collaboration-requests/${requestId}/approve`, + reject: (requestId: string) => `/collaboration-requests/${requestId}/reject`, + }, reports: { superAdmin: (params: Record = {}) => `/reports/superadmin?${toQueryString(params)}`, diff --git a/oudelaa_dashboard/lib/api/mobile-social.ts b/oudelaa_dashboard/lib/api/mobile-social.ts new file mode 100644 index 0000000..b0a9d7e --- /dev/null +++ b/oudelaa_dashboard/lib/api/mobile-social.ts @@ -0,0 +1,173 @@ +import { apiEndpoints } from "@/lib/api/endpoints"; +import { fetchWithAuth } from "@/lib/auth/client"; +import type { + CollaborationRequestItem, + CollaborationRequestsResponse, + DeviceItem, + FollowListResponse, + FollowStatusResponse, + NotificationsResponse, + PostsResponse, + PresenceResponse, + RegisterDevicePayload, + SuccessMessage, + UnregisterDevicePayload, +} from "@/types/api"; + +export async function getUserPresence(userId: string) { + return fetchWithAuth(apiEndpoints.users.publicPresence(userId)); +} + +export async function listUserPosts( + userId: string, + params: Record = {}, +) { + return fetchWithAuth(apiEndpoints.users.publicPosts(userId, params)); +} + +export async function followUser(userId: string) { + return fetchWithAuth(apiEndpoints.users.follow(userId), { + method: "POST", + body: JSON.stringify({}), + }); +} + +export async function unfollowUser(userId: string) { + return fetchWithAuth(apiEndpoints.users.unfollow(userId), { + method: "DELETE", + }); +} + +export async function getFollowStatus(userId: string) { + return fetchWithAuth(apiEndpoints.users.followStatus(userId)); +} + +export async function listUserFollowers( + userId: string, + params: Record = {}, +) { + return fetchWithAuth(apiEndpoints.users.followers(userId, params)); +} + +export async function listUserFollowing( + userId: string, + params: Record = {}, +) { + return fetchWithAuth(apiEndpoints.users.following(userId, params)); +} + +export async function registerDevice(payload: RegisterDevicePayload) { + return fetchWithAuth(apiEndpoints.devices.register, { + method: "POST", + body: JSON.stringify(payload), + }); +} + +export async function unregisterDevice(payload: UnregisterDevicePayload) { + return fetchWithAuth(apiEndpoints.devices.unregister, { + method: "POST", + body: JSON.stringify(payload), + }); +} + +export async function createCollaborationRequest(postId: string, targetUserId: string) { + return fetchWithAuth(apiEndpoints.collaborationRequests.create(postId), { + method: "POST", + body: JSON.stringify({ targetUserId }), + }); +} + +export async function listCollaborationRequests( + params: Record = {}, +) { + return fetchWithAuth(apiEndpoints.collaborationRequests.list(params)); +} + +export async function approveCollaborationRequest(requestId: string) { + return fetchWithAuth(apiEndpoints.collaborationRequests.approve(requestId), { + method: "PATCH", + body: JSON.stringify({}), + }); +} + +export async function rejectCollaborationRequest(requestId: string) { + return fetchWithAuth(apiEndpoints.collaborationRequests.reject(requestId), { + method: "PATCH", + body: JSON.stringify({}), + }); +} + +export async function listMyNotifications( + params: Record = {}, +) { + return fetchWithAuth(apiEndpoints.notifications.mine(params)); +} + +export async function getMyUnreadNotificationCount() { + return fetchWithAuth<{ unreadCount: number }>(apiEndpoints.notifications.unreadCount); +} + +export async function markMyNotificationRead(notificationId: string) { + return fetchWithAuth(apiEndpoints.notifications.markRead(notificationId), { + method: "PATCH", + body: JSON.stringify({}), + }); +} + +export async function markAllMyNotificationsRead() { + return fetchWithAuth(apiEndpoints.notifications.markAllRead, { + method: "PATCH", + body: JSON.stringify({}), + }); +} + +export type MobileSocialRouteGroup = { + title: string; + routes: string[]; +}; + +export const mobileSocialRouteGroups: MobileSocialRouteGroup[] = [ + { + title: "users", + routes: [ + "GET /users/:userId", + "GET /users/:userId/presence", + "GET /users/:userId/posts", + "GET /users/:userId/profile-overview", + ], + }, + { + title: "follows", + routes: [ + "POST /users/:userId/follow", + "DELETE /users/:userId/follow", + "GET /users/:userId/follow-status", + "GET /users/:userId/followers", + "GET /users/:userId/following", + "GET /users/me/followers", + "GET /users/me/following", + ], + }, + { + title: "devices", + routes: ["POST /devices/register", "POST /devices/unregister"], + }, + { + title: "collaborationRequests", + routes: [ + "POST /posts/:postId/collaboration-requests", + "GET /collaboration-requests", + "PATCH /collaboration-requests/:requestId/approve", + "PATCH /collaboration-requests/:requestId/reject", + ], + }, + { + title: "notifications", + routes: [ + "GET /notifications", + "GET /notifications/unread-count", + "PATCH /notifications/:notificationId/read", + "PATCH /notifications/read-all", + ], + }, +]; diff --git a/oudelaa_dashboard/postman/oudelaa-route-map.json b/oudelaa_dashboard/postman/oudelaa-route-map.json index 952572c..553234b 100644 --- a/oudelaa_dashboard/postman/oudelaa-route-map.json +++ b/oudelaa_dashboard/postman/oudelaa-route-map.json @@ -4,7 +4,8 @@ "notes": [ "This dashboard must authenticate with /auth/superadmin/* routes only.", "User-only routes are not a valid contract for this dashboard unless they have a dedicated /admin or /superadmin variant.", - "The dashboard proxy should target the Nest backend API base URL defined in .env.local." + "The dashboard proxy should target the Nest backend API base URL defined in .env.local.", + "Mobile-facing routes are mirrored here for frontend contract visibility, but normal user routes require a normal user access token when called from Flutter." ], "modules": { "auth": [ @@ -15,6 +16,22 @@ "POST /auth/superadmin/sessions/:sessionId/revoke" ], "users": [ + "PATCH /users/me/profile-setup", + "PATCH /users/me/music-setup", + "PATCH /users/me", + "GET /users", + "GET /users/discover", + "GET /users/:userId", + "GET /users/:userId/presence", + "GET /users/:userId/profile-overview", + "GET /users/:userId/posts", + "POST /users/:userId/follow", + "DELETE /users/:userId/follow", + "GET /users/:userId/follow-status", + "GET /users/:userId/followers", + "GET /users/:userId/following", + "GET /users/me/followers", + "GET /users/me/following", "GET /users/admin", "GET /users/admin/admins", "GET /users/admin/:userId", @@ -31,6 +48,23 @@ "PATCH /superadmin/users/:userId/status" ], "posts": [ + "POST /posts", + "POST /posts/reels", + "GET /posts/reels", + "GET /posts/:postId", + "GET /posts/user/:userId", + "PATCH /posts/:postId", + "POST /posts/:postId/repost", + "PATCH /posts/:postId/comment-settings", + "PATCH /posts/:postId/pin-profile", + "PATCH /posts/:postId/unpin-profile", + "PATCH /posts/:postId/archive", + "PATCH /posts/:postId/restore-archive", + "DELETE /posts/:postId", + "POST /posts/:postId/view", + "POST /posts/:postId/play", + "POST /posts/:postId/share", + "POST /posts/:postId/collaboration-requests", "GET /posts/admin/moderation", "DELETE /posts/admin/:postId", "PATCH /superadmin/posts/:postId/status" @@ -41,8 +75,32 @@ "PATCH /superadmin/comments/:commentId/status" ], "notifications": [ + "GET /notifications", + "GET /notifications/unread-count", + "PATCH /notifications/:notificationId/read", + "PATCH /notifications/read-all", "GET /notifications/superadmin" ], + "follows": [ + "POST /follows/toggle", + "GET /follows/followers/:userId", + "GET /follows/following/:userId", + "GET /follows/status/:targetUserId", + "GET /follows/suggestions", + "GET /follows/requests", + "PATCH /follows/requests/:requestId/approve", + "PATCH /follows/requests/:requestId/reject" + ], + "devices": [ + "POST /devices/register", + "POST /devices/unregister" + ], + "collaborationRequests": [ + "POST /posts/:postId/collaboration-requests", + "GET /collaboration-requests", + "PATCH /collaboration-requests/:requestId/approve", + "PATCH /collaboration-requests/:requestId/reject" + ], "marketplace": [ "GET /marketplace/home", "GET /marketplace/listings", diff --git a/oudelaa_dashboard/types/api.ts b/oudelaa_dashboard/types/api.ts index dea51b0..8427531 100644 --- a/oudelaa_dashboard/types/api.ts +++ b/oudelaa_dashboard/types/api.ts @@ -210,8 +210,69 @@ export type SaveStatusResponse = { export type FollowStatusResponse = { following?: boolean; isFollowing?: boolean; + isFollowedBy?: boolean; + isMutual?: boolean; }; +export type FollowListItem = { + _id?: string; + user?: ApiUser; + followerId?: ApiUser | string; + followingId?: ApiUser | string; + status?: "pending" | "accepted" | "rejected"; + createdAt?: string; + updatedAt?: string; +}; + +export type FollowListResponse = PaginatedResponse; + +export type PresenceResponse = { + userId: string; + isOnline: boolean; + lastSeenAt?: string | null; +}; + +export type RegisterDevicePayload = { + fcmToken: string; + platform: "android" | "ios" | "web"; + deviceId?: string; + appVersion?: string; + locale?: string; +}; + +export type UnregisterDevicePayload = { + fcmToken?: string; + deviceId?: string; +}; + +export type DeviceItem = { + _id: string; + userId: string; + fcmToken: string; + platform: "android" | "ios" | "web"; + deviceId?: string; + appVersion?: string; + locale?: string; + isActive?: boolean; + lastSeenAt?: string; + createdAt?: string; + updatedAt?: string; +}; + +export type CollaborationRequestStatus = "pending" | "approved" | "rejected"; + +export type CollaborationRequestItem = { + _id: string; + postId: ApiPost | string; + requesterId: ApiUser | string; + targetUserId: ApiUser | string; + status: CollaborationRequestStatus; + createdAt?: string; + updatedAt?: string; +}; + +export type CollaborationRequestsResponse = PaginatedResponse; + export type NotificationType = | "like" | "comment" @@ -219,7 +280,10 @@ export type NotificationType = | "message" | "save" | "share" - | "mention"; + | "mention" + | "reply" + | "system" + | "collaboration_request"; export type NotificationItem = { _id: string; diff --git a/postman/Oudelaa-Dashboard.postman_collection.json b/postman/Oudelaa-Dashboard.postman_collection.json index 5558a46..b29ccef 100644 --- a/postman/Oudelaa-Dashboard.postman_collection.json +++ b/postman/Oudelaa-Dashboard.postman_collection.json @@ -1,3607 +1,4326 @@ { - "info": { - "name": "Oudelaa Dashboard API", - "description": "Dashboard/admin collection generated from Oudelaa master collection.", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "Auth", - "item": [ - { - "name": "Login", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/login", - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"password\": \"StrongPass123!\"\n}" - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const missing = [];", - "if (!pm.environment.get('accessToken')) missing.push('accessToken');", - "if (!pm.environment.get('postId')) missing.push('postId');", - "if (missing.length) { throw new Error('Missing environment vars: ' + missing.join(', ')); }" - ] - } - }, - { - "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.user).to.exist;", - "const uid = json.user._id || json.user.id;", - "pm.environment.set('accessToken', json.accessToken);", - "pm.environment.set('refreshToken', json.refreshToken);", - "pm.environment.set('userId', uid);", - "pm.environment.set('currentUserId', uid);" - ] - } - } - ] - }, - { - "name": "Target User Login", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/login", - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"{{targetLoginEmail}}\",\n \"password\": \"{{targetLoginPassword}}\"\n}" - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const missing = [];", - "if (!pm.environment.get('accessToken')) missing.push('accessToken');", - "if (!pm.environment.get('postId')) missing.push('postId');", - "if (missing.length) { throw new Error('Missing environment vars: ' + missing.join(', ')); }" - ] - } - }, - { - "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.user).to.exist;", - "const uid = json.user._id || json.user.id;", - "pm.environment.set('targetAccessToken', json.accessToken);", - "pm.environment.set('targetRefreshToken', json.refreshToken);", - "pm.environment.set('targetUserId', uid);" - ] - } - } - ] - }, - { - "name": "Admin Login", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/login", - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"{{adminEmail}}\",\n \"password\": \"{{adminPassword}}\"\n}" - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const missing = [];", - "if (!pm.environment.get('accessToken')) missing.push('accessToken');", - "if (!pm.environment.get('postId')) missing.push('postId');", - "if (missing.length) { throw new Error('Missing environment vars: ' + missing.join(', ')); }" - ] - } - }, - { - "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.environment.set('adminAccessToken', json.accessToken);", - "pm.environment.set('adminRefreshToken', json.refreshToken);", - "pm.environment.set('adminUserId', (json.user && (json.user._id || json.user.id)) || pm.environment.get('adminUserId'));" - ] - } - } - ] - }, - { - "name": "Refresh", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/refresh", - "body": { - "mode": "raw", - "raw": "{\n \"refreshToken\": \"{{refreshToken}}\"\n}" - } - }, - "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.user).to.exist;", - "pm.environment.set('accessToken', json.accessToken);", - "pm.environment.set('refreshToken', json.refreshToken);", - "pm.environment.set('userId', json.user._id || json.user.id);" - ] - } - } - ] - }, - { - "name": "Logout", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/logout", - "body": { - "mode": "raw", - "raw": "{\n \"refreshToken\": \"{{refreshToken}}\"\n}" - } - }, - "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.message).to.eql('Logged out successfully');" - ] - } - } - ] - }, - { - "name": "SuperAdmin", - "item": [ - { - "name": "SuperAdmin Login", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/superadmin/login", - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"admin@oudelaa.com\",\n \"password\": \"SuperAdminStrongPass123!\"\n}" - } - }, - "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.accessToken).to.exist;", - "pm.expect(json.refreshToken).to.exist;", - "pm.environment.set('superAdminAccessToken', json.accessToken);", - "pm.environment.set('superAdminRefreshToken', json.refreshToken);" - ] - } - } - ] - }, - { - "name": "SuperAdmin Refresh", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/superadmin/refresh", - "body": { - "mode": "raw", - "raw": "{\n \"refreshToken\": \"{{superAdminRefreshToken}}\"\n}" - } - }, - "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.accessToken).to.exist;", - "pm.expect(json.refreshToken).to.exist;", - "pm.environment.set('superAdminAccessToken', json.accessToken);", - "pm.environment.set('superAdminRefreshToken', json.refreshToken);" - ] - } - } - ] - }, - { - "name": "SuperAdmin Logout", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/superadmin/logout", - "body": { - "mode": "raw", - "raw": "{\n \"refreshToken\": \"{{superAdminRefreshToken}}\"\n}" - } - }, - "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.message).to.eql('Superadmin logged out successfully');" - ] - } - } - ] - } - ] - }, - { - "name": "SuperAdmin List Sessions", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/auth/superadmin/sessions", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "auth", - "superadmin", - "sessions" - ] - } - } - }, - { - "name": "SuperAdmin Revoke Session", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/auth/superadmin/sessions/{{sessionId}}/revoke", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "auth", - "superadmin", - "sessions", - "{{sessionId}}", - "revoke" - ] - } - } - } - ] - }, - { - "name": "Media", - "item": [ - { - "name": "Media Health", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/media/health" - }, - "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.storage).to.be.an('object');", - "pm.expect(json.storage.provider).to.be.oneOf(['local', 's3']);", - "pm.expect(json.serving.rangeRequests).to.eql(true);", - "pm.expect(json.processing).to.be.an('object');" - ] - } - } - ] - } - ] - }, - { - "name": "Users", - "item": [ - { - "name": "SuperAdmin List Admins", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": "{{baseUrl}}/users/admin/admins?page=1&limit=20" - }, - "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.pagination).to.exist;", - "if (json.items && json.items.length > 0) { pm.environment.set('adminUserId', json.items[0]._id || json.items[0].id); }" - ] - } - } - ] - }, - { - "name": "SuperAdmin Update Admin", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": "{{baseUrl}}/users/admin/admins/{{adminUserId}}", - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"Store Admin Updated\",\n \"stageName\": \"Music Store Owner\",\n \"bio\": \"Admin managing marketplace tools\",\n \"location\": \"Riyadh, Saudi Arabia\"\n}" - } - }, - "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('role');", - "pm.expect(json.role).to.eql('admin');" - ] - } - } - ] - }, - { - "name": "SuperAdmin Delete Admin", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": "{{baseUrl}}/users/admin/admins/{{adminUserId}}" - }, - "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.message).to.eql('Admin deleted successfully');" - ] - } - } - ] - }, - { - "name": "SuperAdmin Create Admin", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": "{{baseUrl}}/users/admin/create-admin", - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"Store Admin\",\n \"username\": \"{{adminUsername}}\",\n \"email\": \"{{adminEmail}}\",\n \"password\": \"{{adminPassword}}\",\n \"confirmPassword\": \"{{adminPassword}}\"\n}" - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.environment.set('adminUserId', json._id || json.id);" - ] - } - } - ] - }, - { - "name": "SuperAdmin Set User Role", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": "{{baseUrl}}/users/admin/{{adminUserId}}/role", - "body": { - "mode": "raw", - "raw": "{\n \"role\": \"admin\"\n}" - } - }, - "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.role).to.eql('admin');" - ] - } - } - ] - }, - { - "name": "Admin Get Users", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": "{{baseUrl}}/users/admin?page=1&limit=10" - }, - "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();", - "if (json.items && json.items.length > 0) { pm.environment.set('adminUserId', json.items[0]._id || json.items[0].id); }" - ] - } - } - ] - }, - { - "name": "Admin Get User By Id", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": "{{baseUrl}}/users/admin/{{adminUserId}}" - }, - "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._id || json.id).to.exist;" - ] - } - } - ] - }, - { - "name": "Admin Update User", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": "{{baseUrl}}/users/admin/{{adminUserId}}", - "body": { - "mode": "raw", - "raw": "{\n \"stageName\": \"Updated by SuperAdmin\",\n \"bio\": \"Profile updated by admin\"\n}" - } - }, - "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.stageName).to.eql('Updated by SuperAdmin');" - ] - } - } - ] - }, - { - "name": "Admin Disable User", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": "{{baseUrl}}/users/admin/{{adminUserId}}/disable", - "body": { - "mode": "raw", - "raw": "{\n \"reason\": \"Violation of community guidelines\"\n}" - } - }, - "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.isDisabled).to.eql(true);" - ] - } - } - ] - }, - { - "name": "Admin Enable User", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": "{{baseUrl}}/users/admin/{{adminUserId}}/enable" - }, - "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.isDisabled).to.eql(false);" - ] - } - } - ] - }, - { - "name": "Admin Delete User", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": "{{baseUrl}}/users/admin/{{adminUserId}}" - }, - "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.message).to.eql('User deleted successfully');" - ] - } - } - ] - }, - { - "name": "Admin Discover Users", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/users/admin/discover?page=1&limit=20&q={{usersQuery}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "users", - "admin", - "discover" - ], - "query": [ - { - "key": "page", - "value": "1" - }, - { - "key": "limit", - "value": "20" - }, - { - "key": "q", - "value": "{{usersQuery}}" - } - ] - } - } - }, - { - "name": "Admin Get User Profile Overview", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/users/admin/{{id}}/profile-overview", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "users", - "admin", - "{{id}}", - "profile-overview" - ] - } - } - } - ] - }, - { - "name": "Posts", - "item": [ - { - "name": "Admin Get Posts Moderation", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/posts/admin/moderation?page=1&limit=20&moderationStatus=flagged", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "posts", - "admin", - "moderation" - ], - "query": [ - { - "key": "page", - "value": "1" - }, - { - "key": "limit", - "value": "20" - }, - { - "key": "moderationStatus", - "value": "flagged" - } - ] - } - } - } - ] - }, - { - "name": "Comments", - "item": [ - { - "name": "Admin Delete Comment", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": "{{baseUrl}}/comments/admin/{{commentId}}" - }, - "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.success).to.eql(true);" - ] - } - } - ] - }, - { - "name": "Admin List Comments", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/comments/admin?page=1&limit=20&moderationStatus=flagged", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "comments", - "admin" - ], - "query": [ - { - "key": "page", - "value": "1" - }, - { - "key": "limit", - "value": "20" - }, - { - "key": "moderationStatus", - "value": "flagged" - } - ] - } - } - } - ] - }, - { - "name": "Notifications", - "item": [ - { - "name": "SuperAdmin Notifications", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/notifications/superadmin?page=1&limit=20&type={{notificationType}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "notifications", - "superadmin" - ], - "query": [ - { - "key": "page", - "value": "1" - }, - { - "key": "limit", - "value": "20" - }, - { - "key": "type", - "value": "{{notificationType}}" - } - ] - } - } - } - ] - }, - { - "name": "Smoke", - "item": [ - { - "name": "Login (Smoke)", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/login", - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"password\": \"StrongPass123!\"\n}" - } - }, - "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();", - "const uid = json.user._id || json.user.id;", - "pm.environment.set('accessToken', json.accessToken);", - "pm.environment.set('refreshToken', json.refreshToken);", - "pm.environment.set('userId', uid);" - ] - } - } - ] - } - ] - }, - { - "name": "Cleanup", - "item": [ - { - "name": "Delete Instrument (if exists)", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{adminAccessToken}}" - } - ], - "url": "{{baseUrl}}/marketplace/admin/instruments/{{instrumentId}}" - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "if (!pm.environment.get('instrumentId')) { postman.setNextRequest(null); }" - ] - } - } - ] - } - ] - }, - { - "name": "Marketplace", - "item": [ - { - "name": "02 Shop Profile", - "item": [ - { - "name": "Admin", - "item": [ - { - "name": "Admin Update Shop Profile", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{adminAccessToken}}" - } - ], - "url": "{{baseUrl}}/marketplace/admin/shop-profile", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "shopName", - "value": "Awtarna Store", - "type": "text" - }, - { - "key": "shopDescription", - "value": "Trusted marketplace shop profile", - "type": "text" - }, - { - "key": "shopLocation", - "value": "Riyadh", - "type": "text" - }, - { - "key": "shopLatitude", - "value": "24.7136", - "type": "text" - }, - { - "key": "shopLongitude", - "value": "46.6753", - "type": "text" - }, - { - "key": "shopImageFiles", - "type": "file", - "src": [], - "disabled": true - }, - { - "key": "shopImageFiles", - "type": "file", - "src": [], - "disabled": true - } - ] - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "if (!pm.environment.get('adminAccessToken')) { throw new Error('Missing environment var: adminAccessToken'); }" - ] - } - }, - { - "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.shopName).to.eql('Oudelaa Music Store');" - ] - } - } - ] - }, - { - "name": "Admin Get My Shop Profile", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{adminAccessToken}}" - } - ], - "url": "{{baseUrl}}/marketplace/admin/shop-profile/me" - }, - "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('shopName');" - ] - } - } - ] - } - ] - }, - { - "name": "Public", - "item": [ - { - "name": "Public Get Shop By Admin Id", - "request": { - "method": "GET", - "header": [], - "url": "{{baseUrl}}/marketplace/shops/{{adminUserId}}" - }, - "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('shopName');" - ] - } - } - ] - } - ] - } - ] - }, - { - "name": "03 Repair Shops", - "item": [ - { - "name": "Admin", - "item": [ - { - "name": "Admin Create Repair Shop (One Shop Per Admin)", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{adminAccessToken}}" - } - ], - "url": "{{baseUrl}}/marketplace/admin/repair-shops", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "name", - "value": "Fix Strings Workshop", - "type": "text" - }, - { - "key": "description", - "value": "Repair shop for oud and violin", - "type": "text" - }, - { - "key": "services", - "value": "repair,setup,cleaning", - "type": "text" - }, - { - "key": "phone", - "value": "+966500000000", - "type": "text" - }, - { - "key": "whatsapp", - "value": "+966500000000", - "type": "text" - }, - { - "key": "location", - "value": "Riyadh", - "type": "text" - }, - { - "key": "latitude", - "value": "24.7136", - "type": "text" - }, - { - "key": "longitude", - "value": "46.6753", - "type": "text" - }, - { - "key": "isActive", - "value": "true", - "type": "text" - }, - { - "key": "imageFiles", - "type": "file", - "src": [], - "disabled": true - }, - { - "key": "imageFiles", - "type": "file", - "src": [], - "disabled": true - } - ] - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "if (!pm.environment.get('adminAccessToken')) { throw new Error('Missing environment var: adminAccessToken'); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.environment.set('repairShopId', json._id || json.id);", - "pm.expect(json.name).to.eql('Oudelaa Repair Center');" - ] - } - } - ] - }, - { - "name": "Admin Create Second Repair Shop Should Fail", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{adminAccessToken}}" - } - ], - "url": "{{baseUrl}}/marketplace/admin/repair-shops", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "name", - "value": "Fix Strings Workshop", - "type": "text" - }, - { - "key": "description", - "value": "Repair shop for oud and violin", - "type": "text" - }, - { - "key": "services", - "value": "repair,setup,cleaning", - "type": "text" - }, - { - "key": "phone", - "value": "+966500000000", - "type": "text" - }, - { - "key": "whatsapp", - "value": "+966500000000", - "type": "text" - }, - { - "key": "location", - "value": "Riyadh", - "type": "text" - }, - { - "key": "latitude", - "value": "24.7136", - "type": "text" - }, - { - "key": "longitude", - "value": "46.6753", - "type": "text" - }, - { - "key": "isActive", - "value": "true", - "type": "text" - }, - { - "key": "imageFiles", - "type": "file", - "src": [], - "disabled": true - }, - { - "key": "imageFiles", - "type": "file", - "src": [], - "disabled": true - } - ] - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "if (!pm.environment.get('adminAccessToken')) { throw new Error('Missing environment var: adminAccessToken'); }", - "if (!pm.environment.get('repairShopId')) { throw new Error('Create the first repair shop before running this negative test.'); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 400', function () { pm.response.to.have.status(400); });", - "const json = pm.response.json();", - "const messages = Array.isArray(json.message) ? json.message : [json.message];", - "pm.expect(messages.join(' ')).to.include('Each admin can create one marketplace shop only');" - ] - } - } - ] - }, - { - "name": "Admin Update Repair Shop", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{adminAccessToken}}" - } - ], - "url": "{{baseUrl}}/marketplace/admin/repair-shops/{{repairShopId}}", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "name", - "value": "Updated Fix Strings Workshop", - "type": "text" - }, - { - "key": "description", - "value": "Updated repair shop description", - "type": "text" - }, - { - "key": "services", - "value": "repair,setup,polishing", - "type": "text" - }, - { - "key": "phone", - "value": "+966511111111", - "type": "text" - }, - { - "key": "whatsapp", - "value": "+966511111111", - "type": "text" - }, - { - "key": "location", - "value": "Jeddah", - "type": "text" - }, - { - "key": "latitude", - "value": "21.5433", - "type": "text" - }, - { - "key": "longitude", - "value": "39.1728", - "type": "text" - }, - { - "key": "isActive", - "value": "true", - "type": "text" - }, - { - "key": "imageFiles", - "type": "file", - "src": [], - "disabled": true - }, - { - "key": "imageFiles", - "type": "file", - "src": [], - "disabled": true - } - ] - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" - ] - } - } - ] - }, - { - "name": "Admin Get My Repair Shops", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{adminAccessToken}}" - } - ], - "url": "{{baseUrl}}/marketplace/admin/repair-shops/me?q={{repairShopQuery}}&isActive={{repairShopIsActive}}&sortBy={{repairShopSortBy}}&sortOrder={{listSortOrder}}&page=1&limit=20" - }, - "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.pagination).to.exist;", - "if (json.items && json.items.length > 0) { pm.environment.set('repairShopId', json.items[0]._id || json.items[0].id); }" - ] - } - } - ] - }, - { - "name": "Admin Delete Repair Shop", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{adminAccessToken}}" - } - ], - "url": "{{baseUrl}}/marketplace/admin/repair-shops/{{repairShopId}}" - }, - "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.success).to.eql(true);" - ] - } - } - ] - } - ] - } - ] - }, - { - "name": "04 Musical Instruments", - "item": [ - { - "name": "Admin", - "item": [ - { - "name": "Admin Create Instrument", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{adminAccessToken}}" - } - ], - "url": "{{baseUrl}}/marketplace/admin/instruments", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "title", - "value": "Concert Guitar", - "type": "text" - }, - { - "key": "description", - "value": "Acoustic guitar in excellent condition", - "type": "text" - }, - { - "key": "price", - "value": "2100", - "type": "text" - }, - { - "key": "currency", - "value": "SAR", - "type": "text" - }, - { - "key": "quantity", - "value": "1", - "type": "text" - }, - { - "key": "condition", - "value": "used", - "type": "text" - }, - { - "key": "instrumentType", - "value": "Guitar", - "type": "text" - }, - { - "key": "imageFiles", - "type": "file", - "src": [], - "disabled": true - }, - { - "key": "imageFiles", - "type": "file", - "src": [], - "disabled": true - } - ] - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "if (!pm.environment.get('adminAccessToken')) { throw new Error('Missing environment var: adminAccessToken'); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.environment.set('instrumentId', json._id || json.id);" - ] - } - } - ] - }, - { - "name": "Admin Update Instrument", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{adminAccessToken}}" - } - ], - "url": "{{baseUrl}}/marketplace/admin/instruments/{{instrumentId}}", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "title", - "value": "Updated Concert Guitar", - "type": "text" - }, - { - "key": "description", - "value": "Updated instrument description", - "type": "text" - }, - { - "key": "price", - "value": "2400", - "type": "text" - }, - { - "key": "currency", - "value": "SAR", - "type": "text" - }, - { - "key": "quantity", - "value": "1", - "type": "text" - }, - { - "key": "condition", - "value": "used", - "type": "text" - }, - { - "key": "instrumentType", - "value": "Violin", - "type": "text" - }, - { - "key": "imageFiles", - "type": "file", - "src": [], - "disabled": true - }, - { - "key": "imageFiles", - "type": "file", - "src": [], - "disabled": true - } - ] - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" - ] - } - } - ] - }, - { - "name": "Admin Get My Instruments", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{adminAccessToken}}" - } - ], - "url": "{{baseUrl}}/marketplace/admin/instruments/me?q={{marketplaceQuery}}&minPrice={{marketplaceMinPrice}}&maxPrice={{marketplaceMaxPrice}}&isActive={{marketplaceIsActive}}&condition={{marketplaceCondition}}&instrumentType={{marketplaceInstrumentType}}&sortBy={{marketplaceSortBy}}&sortOrder={{listSortOrder}}&page=1&limit=20" - }, - "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.pagination).to.exist;", - "if (json.items && json.items.length > 0) { pm.environment.set('instrumentId', json.items[0]._id || json.items[0].id); }" - ] - } - } - ] - }, - { - "name": "Admin Delete Instrument", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{adminAccessToken}}" - } - ], - "url": "{{baseUrl}}/marketplace/admin/instruments/{{instrumentId}}" - }, - "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.success).to.eql(true);" - ] - } - } - ] - } - ] - } - ] - }, - { - "name": "05 General Marketplace", - "item": [ - { - "name": "Admin", - "item": [ - { - "name": "Admin Create Listing", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{adminAccessToken}}" - } - ], - "url": "{{baseUrl}}/marketplace/admin/listings", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "title", - "value": "Microphone SM58", - "type": "text" - }, - { - "key": "description", - "value": "Durable dynamic microphone for studio and stage", - "type": "text" - }, - { - "key": "price", - "value": "450", - "type": "text" - }, - { - "key": "currency", - "value": "SAR", - "type": "text" - }, - { - "key": "quantity", - "value": "1", - "type": "text" - }, - { - "key": "condition", - "value": "used", - "type": "text" - }, - { - "key": "instrumentType", - "value": "Microphone", - "type": "text" - }, - { - "key": "listingCategory", - "value": "audio_gear", - "type": "text" - }, - { - "key": "isActive", - "value": "true", - "type": "text" - }, - { - "key": "imageFiles", - "type": "file", - "src": [], - "disabled": true - }, - { - "key": "imageFiles", - "type": "file", - "src": [], - "disabled": true - } - ] - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "if (!pm.environment.get('adminAccessToken')) { throw new Error('Missing environment var: adminAccessToken'); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.environment.set('listingId', json._id || json.id);", - "pm.expect(json.listingCategory).to.eql('accessory');" - ] - } - } - ] - }, - { - "name": "Admin Update Listing", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{adminAccessToken}}" - } - ], - "url": "{{baseUrl}}/marketplace/admin/listings/{{listingId}}", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "title", - "value": "Updated Microphone SM58", - "type": "text" - }, - { - "key": "description", - "value": "Updated marketplace listing description", - "type": "text" - }, - { - "key": "price", - "value": "500", - "type": "text" - }, - { - "key": "currency", - "value": "SAR", - "type": "text" - }, - { - "key": "quantity", - "value": "1", - "type": "text" - }, - { - "key": "condition", - "value": "used", - "type": "text" - }, - { - "key": "instrumentType", - "value": "Microphone", - "type": "text" - }, - { - "key": "listingCategory", - "value": "audio_gear", - "type": "text" - }, - { - "key": "isActive", - "value": "true", - "type": "text" - }, - { - "key": "imageFiles", - "type": "file", - "src": [], - "disabled": true - }, - { - "key": "imageFiles", - "type": "file", - "src": [], - "disabled": true - } - ] - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" - ] - } - } - ] - }, - { - "name": "Admin Get My Listings", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{adminAccessToken}}" - } - ], - "url": "{{baseUrl}}/marketplace/admin/listings/me?q={{marketplaceQuery}}&minPrice={{marketplaceMinPrice}}&maxPrice={{marketplaceMaxPrice}}&isActive={{marketplaceIsActive}}&listingCategory={{marketplaceListingCategory}}&condition={{marketplaceCondition}}&instrumentType={{marketplaceInstrumentType}}&sortBy={{marketplaceSortBy}}&sortOrder={{listSortOrder}}&page=1&limit=20" - }, - "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.pagination).to.exist;", - "if (json.items && json.items.length > 0) { pm.environment.set('listingId', json.items[0]._id || json.items[0].id); }" - ] - } - } - ] - }, - { - "name": "Admin Delete Listing", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{adminAccessToken}}" - } - ], - "url": "{{baseUrl}}/marketplace/admin/listings/{{listingId}}" - }, - "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.success).to.eql(true);" - ] - } - } - ] - } - ] - } - ] - }, - { - "name": "SuperAdmin List Marketplace Listings", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/marketplace/superadmin/listings?page=1&limit=20", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "marketplace", - "superadmin", - "listings" - ], - "query": [ - { - "key": "page", - "value": "1" - }, - { - "key": "limit", - "value": "20" - } - ] - } - } - }, - { - "name": "SuperAdmin Update Listing Status", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/marketplace/superadmin/listings/{{id}}/status", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "marketplace", - "superadmin", - "listings", - "{{id}}", - "status" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"isActive\": false,\n \"moderationStatus\": \"flagged\",\n \"moderationReason\": \"Needs marketplace review\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "SuperAdmin Delete Listing", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/marketplace/superadmin/listings/{{id}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "marketplace", - "superadmin", - "listings", - "{{id}}" - ] - } - } - }, - { - "name": "SuperAdmin List Repair Shops", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/marketplace/superadmin/repair-shops?page=1&limit=20", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "marketplace", - "superadmin", - "repair-shops" - ], - "query": [ - { - "key": "page", - "value": "1" - }, - { - "key": "limit", - "value": "20" - } - ] - } - } - }, - { - "name": "SuperAdmin Update Repair Shop Status", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/marketplace/superadmin/repair-shops/{{id}}/status", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "marketplace", - "superadmin", - "repair-shops", - "{{id}}", - "status" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"isActive\": false,\n \"moderationStatus\": \"flagged\",\n \"moderationReason\": \"Needs marketplace review\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "SuperAdmin Delete Repair Shop", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/marketplace/superadmin/repair-shops/{{id}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "marketplace", - "superadmin", - "repair-shops", - "{{id}}" - ] - } - } - }, - { - "name": "SuperAdmin Create Listing For Admin", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/marketplace/superadmin/admins/{{adminId}}/listings", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "marketplace", - "superadmin", - "admins", - "{{adminId}}", - "listings" - ] - }, - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "title", - "value": "Superadmin listing", - "type": "text" - }, - { - "key": "description", - "value": "Created by superadmin", - "type": "text" - }, - { - "key": "price", - "value": "1000", - "type": "text" - }, - { - "key": "currency", - "value": "SAR", - "type": "text" - }, - { - "key": "quantity", - "value": "1", - "type": "text" - }, - { - "key": "listingCategory", - "value": "general", - "type": "text" - } - ] - } - } - }, - { - "name": "SuperAdmin Create Instrument For Admin", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/marketplace/superadmin/admins/{{adminId}}/instruments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "marketplace", - "superadmin", - "admins", - "{{adminId}}", - "instruments" - ] - }, - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "title", - "value": "Superadmin instrument", - "type": "text" - }, - { - "key": "description", - "value": "Created by superadmin", - "type": "text" - }, - { - "key": "price", - "value": "1200", - "type": "text" - }, - { - "key": "currency", - "value": "SAR", - "type": "text" - }, - { - "key": "quantity", - "value": "1", - "type": "text" - }, - { - "key": "instrumentType", - "value": "Oud", - "type": "text" - } - ] - } - } - }, - { - "name": "SuperAdmin Create Repair Shop For Admin", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/marketplace/superadmin/admins/{{adminId}}/repair-shops", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "marketplace", - "superadmin", - "admins", - "{{adminId}}", - "repair-shops" - ] - }, - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "name", - "value": "Superadmin Repair Shop", - "type": "text" - }, - { - "key": "description", - "value": "Created by superadmin", - "type": "text" - }, - { - "key": "services", - "value": "oud repair,string replacement", - "type": "text" - }, - { - "key": "location", - "value": "Riyadh", - "type": "text" - } - ] - } - } - }, - { - "name": "SuperAdmin Update Admin Shop Profile", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/marketplace/superadmin/admins/{{adminId}}/shop-profile", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "marketplace", - "superadmin", - "admins", - "{{adminId}}", - "shop-profile" - ] - }, - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "shopName", - "value": "Superadmin Shop", - "type": "text" - }, - { - "key": "shopDescription", - "value": "Updated by superadmin", - "type": "text" - }, - { - "key": "shopLocation", - "value": "Riyadh", - "type": "text" - } - ] - } - } - } - ] - }, - { - "name": "Reports", - "item": [ - { - "name": "Superadmin Reports", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": "{{baseUrl}}/reports/superadmin?status=open" - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" - ] - } - } - ] - }, - { - "name": "Superadmin Update Report Status", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/reports/superadmin/{{reportId}}/status", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "reports", - "superadmin", - "{{reportId}}", - "status" - ], - "query": [] - }, - "body": { - "mode": "raw", - "raw": "{\n \"status\": \"in_review\",\n \"resolutionNote\": \"Under review\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - } - ] - }, - { - "name": "Audit", - "item": [ - { - "name": "SuperAdmin Audit Logs", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/audit/superadmin/logs?page=1&limit=20&action={{auditAction}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "audit", - "superadmin", - "logs" - ], - "query": [ - { - "key": "page", - "value": "1" - }, - { - "key": "limit", - "value": "20" - }, - { - "key": "action", - "value": "{{auditAction}}" - } - ] - } - } - } - ] - }, - { - "name": "SuperAdmin", - "item": [ - { - "name": "Get SuperAdmin Session", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/session", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "session" - ] - } - } - }, - { - "name": "Get SuperAdmin Overview", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/overview", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "overview" - ] - } - } - }, - { - "name": "Get SuperAdmin Charts", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/charts?range=30d", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "charts" - ], - "query": [ - { - "key": "range", - "value": "30d" - } - ] - } - } - }, - { - "name": "Get SuperAdmin Recent Activity", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/recent-activity?page=1&limit=20", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "recent-activity" - ], - "query": [ - { - "key": "page", - "value": "1" - }, - { - "key": "limit", - "value": "20" - } - ] - } - } - }, - { - "name": "Get SuperAdmin Reports", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/reports?range=30d", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "reports" - ], - "query": [ - { - "key": "range", - "value": "30d" - } - ] - } - } - }, - { - "name": "Get SuperAdmin Ops", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/ops", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "ops" - ] - } - } - }, - { - "name": "List SuperAdmin Cases", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/cases?page=1&limit=20&status=open", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "cases" - ], - "query": [ - { - "key": "page", - "value": "1" - }, - { - "key": "limit", - "value": "20" - }, - { - "key": "status", - "value": "open" - } - ] - } - } - }, - { - "name": "Create SuperAdmin Case", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/cases", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "cases" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"title\": \"Manual review\",\n \"description\": \"Created from Postman\",\n \"targetType\": \"post\",\n \"targetId\": \"{{postId}}\",\n \"priority\": \"medium\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "Update SuperAdmin Case", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/cases/{{caseId}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "cases", - "{{caseId}}" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"status\": \"in_review\",\n \"resolutionNote\": \"Updated from Postman\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "Perform SuperAdmin Bulk Action", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/bulk-actions", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "bulk-actions" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"targetType\": \"post\",\n \"targetIds\": [\n \"{{bulkActionTargetId}}\"\n ],\n \"action\": \"flag\",\n \"reason\": \"Bulk moderation from Postman\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "Get SuperAdmin Settings", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/settings", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "settings" - ] - } - } - }, - { - "name": "Get SuperAdmin Settings History", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/settings/history?page=1&limit=20", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "settings", - "history" - ], - "query": [ - { - "key": "page", - "value": "1" - }, - { - "key": "limit", - "value": "20" - } - ] - } - } - }, - { - "name": "Update SuperAdmin Settings", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/settings", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "settings" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"maintenanceMode\": false\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "Restore SuperAdmin Settings Version", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/settings/history/{{historyId}}/restore", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "settings", - "history", - "{{historyId}}", - "restore" - ] - } - } - }, - { - "name": "Update Post Moderation Status", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/posts/{{postId}}/status", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "posts", - "{{postId}}", - "status" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"status\": \"flagged\",\n \"reason\": \"Needs review\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "Delete Post By SuperAdmin", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/posts/{{postId}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "posts", - "{{postId}}" - ] - } - } - }, - { - "name": "Restore Post By SuperAdmin", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/posts/{{postId}}/restore", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "posts", - "{{postId}}", - "restore" - ] - } - } - }, - { - "name": "Update Comment Moderation Status", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/comments/{{commentId}}/status", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "comments", - "{{commentId}}", - "status" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"status\": \"flagged\",\n \"reason\": \"Needs review\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "Delete Comment By SuperAdmin", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/comments/{{commentId}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "comments", - "{{commentId}}" - ] - } - } - }, - { - "name": "Restore Comment By SuperAdmin", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/comments/{{commentId}}/restore", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "comments", - "{{commentId}}", - "restore" - ] - } - } - }, - { - "name": "Update User Status By SuperAdmin", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{superAdminAccessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/superadmin/users/{{userId}}/status", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "superadmin", - "users", - "{{userId}}", - "status" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"isDisabled\": true,\n \"reason\": \"Policy violation\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - } - ] - } - ], - "variable": [ - { - "key": "baseUrl", - "value": "http://localhost:4000/api/v1", - "type": "string" - }, - { - "key": "accessToken", - "value": "", - "type": "string" - }, - { - "key": "refreshToken", - "value": "", - "type": "string" - }, - { - "key": "superAdminAccessToken", - "value": "", - "type": "string" - }, - { - "key": "superAdminRefreshToken", - "value": "", - "type": "string" - }, - { - "key": "targetAccessToken", - "value": "", - "type": "string" - }, - { - "key": "targetRefreshToken", - "value": "", - "type": "string" - }, - { - "key": "userId", - "value": "", - "type": "string" - }, - { - "key": "currentUserId", - "value": "", - "type": "string" - }, - { - "key": "targetUserId", - "value": "", - "type": "string" - }, - { - "key": "targetUsername", - "value": "", - "type": "string" - }, - { - "key": "lastFollowingState", - "value": "", - "type": "string" - }, - { - "key": "notificationUnreadCount", - "value": "", - "type": "string" - }, - { - "key": "notificationId", - "value": "", - "type": "string" - }, - { - "key": "postId", - "value": "", - "type": "string" - }, - { - "key": "ownPostId", - "value": "", - "type": "string" - }, - { - "key": "targetPostId", - "value": "", - "type": "string" - }, - { - "key": "adminPostId", - "value": "", - "type": "string" - }, - { - "key": "commentId", - "value": "", - "type": "string" - }, - { - "key": "ownCommentId", - "value": "", - "type": "string" - }, - { - "key": "replyCommentId", - "value": "", - "type": "string" - }, - { - "key": "conversationId", - "value": "", - "type": "string" - }, - { - "key": "messageId", - "value": "", - "type": "string" - }, - { - "key": "sessionJti", - "value": "", - "type": "string" - }, - { - "key": "registerEmail", - "value": "", - "type": "string" - }, - { - "key": "registerUsername", - "value": "", - "type": "string" - }, - { - "key": "adminUserId", - "value": "", - "type": "string" - }, - { - "key": "targetLoginEmail", - "value": "", - "type": "string" - }, - { - "key": "targetLoginPassword", - "value": "", - "type": "string" - }, - { - "key": "resetCode", - "value": "", - "type": "string" - }, - { - "key": "resetToken", - "value": "", - "type": "string" - }, - { - "key": "emailVerificationCode", - "value": "", - "type": "string" - }, - { - "key": "googleIdToken", - "value": "", - "type": "string" - }, - { - "key": "newPassword", - "value": "NewStrongPass123!", - "type": "string" - }, - { - "key": "feedCursor", - "value": "", - "type": "string" - }, - { - "key": "chatConversationsCursor", - "value": "", - "type": "string" - }, - { - "key": "chatMessagesCursor", - "value": "", - "type": "string" - }, - { - "key": "adminAccessToken", - "value": "", - "type": "string" - }, - { - "key": "adminRefreshToken", - "value": "", - "type": "string" - }, - { - "key": "adminEmail", - "value": "store_admin@example.com", - "type": "string" - }, - { - "key": "adminUsername", - "value": "store_admin_01", - "type": "string" - }, - { - "key": "adminPassword", - "value": "AdminStrongPass123!", - "type": "string" - }, - { - "key": "usersQuery", - "value": "user", - "type": "string" - }, - { - "key": "usersVerified", - "value": "true", - "type": "string" - }, - { - "key": "usersSortBy", - "value": "createdAt", - "type": "string" - }, - { - "key": "listSortOrder", - "value": "desc", - "type": "string" - }, - { - "key": "commentSortBy", - "value": "createdAt", - "type": "string" - }, - { - "key": "postVisibility", - "value": "public", - "type": "string" - }, - { - "key": "postSortBy", - "value": "createdAt", - "type": "string" - }, - { - "key": "postTypeFilter", - "value": "image", - "type": "string" - }, - { - "key": "postSearchQuery", - "value": "music", - "type": "string" - }, - { - "key": "postHashtag", - "value": "music", - "type": "string" - }, - { - "key": "reelQuery", - "value": "reel", - "type": "string" - }, - { - "key": "notificationRead", - "value": "false", - "type": "string" - }, - { - "key": "notificationType", - "value": "mention", - "type": "string" - }, - { - "key": "notificationResourceType", - "value": "post", - "type": "string" - }, - { - "key": "feedPreferredPostType", - "value": "video", - "type": "string" - }, - { - "key": "feedIncludeSuggestions", - "value": "true", - "type": "string" - }, - { - "key": "feedSuggestionInterval", - "value": "4", - "type": "string" - }, - { - "key": "feedFollowingOnly", - "value": "false", - "type": "string" - }, - { - "key": "feedRadiusKm", - "value": "30", - "type": "string" - }, - { - "key": "homeFeedHasMixedItems", - "value": "", - "type": "string" - }, - { - "key": "lastViewCount", - "value": "", - "type": "string" - }, - { - "key": "lastPlayCount", - "value": "", - "type": "string" - }, - { - "key": "lastShareCount", - "value": "", - "type": "string" - }, - { - "key": "marketplaceQuery", - "value": "oud", - "type": "string" - }, - { - "key": "marketplaceMinPrice", - "value": "1000", - "type": "string" - }, - { - "key": "marketplaceMaxPrice", - "value": "5000", - "type": "string" - }, - { - "key": "marketplaceIsActive", - "value": "true", - "type": "string" - }, - { - "key": "marketplaceSortBy", - "value": "price", - "type": "string" - }, - { - "key": "repairShopQuery", - "value": "riyadh", - "type": "string" - }, - { - "key": "repairShopIsActive", - "value": "true", - "type": "string" - }, - { - "key": "repairShopSortBy", - "value": "name", - "type": "string" - }, - { - "key": "instrumentId", - "value": "", - "type": "string" - }, - { - "key": "repairShopId", - "value": "", - "type": "string" - }, - { - "key": "generatedMusicUrl", - "value": "", - "type": "string" - }, - { - "key": "aiMusicPrompt", - "value": "Calm oud melody with light percussion and emotional Arabic mood", - "type": "string" - }, - { - "key": "aiMusicDuration", - "value": "12", - "type": "string" - }, - { - "key": "aiMusicSeed", - "value": "42", - "type": "string" - }, - { - "key": "listingId", - "value": "", - "type": "string" - }, - { - "key": "marketplaceListingCategory", - "value": "accessory", - "type": "string" - }, - { - "key": "marketplaceCondition", - "value": "used", - "type": "string" - }, - { - "key": "marketplaceInstrumentType", - "value": "oud", - "type": "string" - }, - { - "key": "talentRole", - "value": "instrumentalist", - "type": "string" - }, - { - "key": "talentExperienceLevel", - "value": "", - "type": "string" - }, - { - "key": "usersHasAvatar", - "value": "true", - "type": "string" - }, - { - "key": "reactionType", - "value": "love", - "type": "string" - }, - { - "key": "followRequestId", - "value": "", - "type": "string" - }, - { - "key": "reportId", - "value": "", - "type": "string" - }, - { - "key": "repostId", - "value": "", - "type": "string" - }, - { - "key": "quotePostId", - "value": "", - "type": "string" - }, - { - "key": "replyToMessageId", - "value": "" - }, - { - "key": "mediaFilePath", - "value": "" - }, - { - "key": "collaboratorId", - "value": "" - }, - { - "key": "commentKeyword", - "value": "spam" - }, - { - "key": "reportReason", - "value": "spam" - }, - { - "key": "sessionId", - "value": "" - }, - { - "key": "caseId", - "value": "" - }, - { - "key": "historyId", - "value": "" - }, - { - "key": "bulkActionTargetId", - "value": "" - }, - { - "key": "auditAction", - "value": "" - } - ] -} + "info": { + "name": "Oudelaa Dashboard API", + "description": "Dashboard/admin collection generated from Oudelaa master collection.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Auth", + "item": [ + { + "name": "Login", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/login", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"password\": \"StrongPass123!\"\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const missing = [];", + "if (!pm.environment.get(\u0027accessToken\u0027)) missing.push(\u0027accessToken\u0027);", + "if (!pm.environment.get(\u0027postId\u0027)) missing.push(\u0027postId\u0027);", + "if (missing.length) { throw new Error(\u0027Missing environment vars: \u0027 + missing.join(\u0027, \u0027)); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.user).to.exist;", + "const uid = json.user._id || json.user.id;", + "pm.environment.set(\u0027accessToken\u0027, json.accessToken);", + "pm.environment.set(\u0027refreshToken\u0027, json.refreshToken);", + "pm.environment.set(\u0027userId\u0027, uid);", + "pm.environment.set(\u0027currentUserId\u0027, uid);" + ] + } + } + ] + }, + { + "name": "Target User Login", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/login", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{targetLoginEmail}}\",\n \"password\": \"{{targetLoginPassword}}\"\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const missing = [];", + "if (!pm.environment.get(\u0027accessToken\u0027)) missing.push(\u0027accessToken\u0027);", + "if (!pm.environment.get(\u0027postId\u0027)) missing.push(\u0027postId\u0027);", + "if (missing.length) { throw new Error(\u0027Missing environment vars: \u0027 + missing.join(\u0027, \u0027)); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.user).to.exist;", + "const uid = json.user._id || json.user.id;", + "pm.environment.set(\u0027targetAccessToken\u0027, json.accessToken);", + "pm.environment.set(\u0027targetRefreshToken\u0027, json.refreshToken);", + "pm.environment.set(\u0027targetUserId\u0027, uid);" + ] + } + } + ] + }, + { + "name": "Admin Login", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/login", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{adminEmail}}\",\n \"password\": \"{{adminPassword}}\"\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const missing = [];", + "if (!pm.environment.get(\u0027accessToken\u0027)) missing.push(\u0027accessToken\u0027);", + "if (!pm.environment.get(\u0027postId\u0027)) missing.push(\u0027postId\u0027);", + "if (missing.length) { throw new Error(\u0027Missing environment vars: \u0027 + missing.join(\u0027, \u0027)); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.environment.set(\u0027adminAccessToken\u0027, json.accessToken);", + "pm.environment.set(\u0027adminRefreshToken\u0027, json.refreshToken);", + "pm.environment.set(\u0027adminUserId\u0027, (json.user \u0026\u0026 (json.user._id || json.user.id)) || pm.environment.get(\u0027adminUserId\u0027));" + ] + } + } + ] + }, + { + "name": "Refresh", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/refresh", + "body": { + "mode": "raw", + "raw": "{\n \"refreshToken\": \"{{refreshToken}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.user).to.exist;", + "pm.environment.set(\u0027accessToken\u0027, json.accessToken);", + "pm.environment.set(\u0027refreshToken\u0027, json.refreshToken);", + "pm.environment.set(\u0027userId\u0027, json.user._id || json.user.id);" + ] + } + } + ] + }, + { + "name": "Logout", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/logout", + "body": { + "mode": "raw", + "raw": "{\n \"refreshToken\": \"{{refreshToken}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.eql(\u0027Logged out successfully\u0027);" + ] + } + } + ] + }, + { + "name": "SuperAdmin", + "item": [ + { + "name": "SuperAdmin Login", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/superadmin/login", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"admin@oudelaa.com\",\n \"password\": \"SuperAdminStrongPass123!\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.accessToken).to.exist;", + "pm.expect(json.refreshToken).to.exist;", + "pm.environment.set(\u0027superAdminAccessToken\u0027, json.accessToken);", + "pm.environment.set(\u0027superAdminRefreshToken\u0027, json.refreshToken);" + ] + } + } + ] + }, + { + "name": "SuperAdmin Refresh", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/superadmin/refresh", + "body": { + "mode": "raw", + "raw": "{\n \"refreshToken\": \"{{superAdminRefreshToken}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.accessToken).to.exist;", + "pm.expect(json.refreshToken).to.exist;", + "pm.environment.set(\u0027superAdminAccessToken\u0027, json.accessToken);", + "pm.environment.set(\u0027superAdminRefreshToken\u0027, json.refreshToken);" + ] + } + } + ] + }, + { + "name": "SuperAdmin Logout", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/superadmin/logout", + "body": { + "mode": "raw", + "raw": "{\n \"refreshToken\": \"{{superAdminRefreshToken}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.eql(\u0027Superadmin logged out successfully\u0027);" + ] + } + } + ] + } + ] + }, + { + "name": "SuperAdmin List Sessions", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/auth/superadmin/sessions", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "superadmin", + "sessions" + ] + } + } + }, + { + "name": "SuperAdmin Revoke Session", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/auth/superadmin/sessions/{{sessionId}}/revoke", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "superadmin", + "sessions", + "{{sessionId}}", + "revoke" + ] + } + } + } + ] + }, + { + "name": "Media", + "item": [ + { + "name": "Media Health", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/media/health" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.storage).to.be.an(\u0027object\u0027);", + "pm.expect(json.storage.provider).to.be.oneOf([\u0027local\u0027, \u0027s3\u0027]);", + "pm.expect(json.serving.rangeRequests).to.eql(true);", + "pm.expect(json.processing).to.be.an(\u0027object\u0027);" + ] + } + } + ] + } + ] + }, + { + "name": "Users", + "item": [ + { + "name": "SuperAdmin List Admins", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/admins?page=1\u0026limit=20" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;", + "if (json.items \u0026\u0026 json.items.length \u003e 0) { pm.environment.set(\u0027adminUserId\u0027, json.items[0]._id || json.items[0].id); }" + ] + } + } + ] + }, + { + "name": "SuperAdmin Update Admin", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/admins/{{adminUserId}}", + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Store Admin Updated\",\n \"stageName\": \"Music Store Owner\",\n \"bio\": \"Admin managing marketplace tools\",\n \"location\": \"Riyadh, Saudi Arabia\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property(\u0027role\u0027);", + "pm.expect(json.role).to.eql(\u0027admin\u0027);" + ] + } + } + ] + }, + { + "name": "SuperAdmin Delete Admin", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/admins/{{adminUserId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.eql(\u0027Admin deleted successfully\u0027);" + ] + } + } + ] + }, + { + "name": "SuperAdmin Create Admin", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/create-admin", + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Store Admin\",\n \"username\": \"{{adminUsername}}\",\n \"email\": \"{{adminEmail}}\",\n \"password\": \"{{adminPassword}}\",\n \"confirmPassword\": \"{{adminPassword}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.environment.set(\u0027adminUserId\u0027, json._id || json.id);" + ] + } + } + ] + }, + { + "name": "SuperAdmin Set User Role", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/{{adminUserId}}/role", + "body": { + "mode": "raw", + "raw": "{\n \"role\": \"admin\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.role).to.eql(\u0027admin\u0027);" + ] + } + } + ] + }, + { + "name": "Admin Get Users", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin?page=1\u0026limit=10" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "if (json.items \u0026\u0026 json.items.length \u003e 0) { pm.environment.set(\u0027adminUserId\u0027, json.items[0]._id || json.items[0].id); }" + ] + } + } + ] + }, + { + "name": "Admin Get User By Id", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/{{adminUserId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json._id || json.id).to.exist;" + ] + } + } + ] + }, + { + "name": "Admin Update User", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/{{adminUserId}}", + "body": { + "mode": "raw", + "raw": "{\n \"stageName\": \"Updated by SuperAdmin\",\n \"bio\": \"Profile updated by admin\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.stageName).to.eql(\u0027Updated by SuperAdmin\u0027);" + ] + } + } + ] + }, + { + "name": "Admin Disable User", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/{{adminUserId}}/disable", + "body": { + "mode": "raw", + "raw": "{\n \"reason\": \"Violation of community guidelines\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.isDisabled).to.eql(true);" + ] + } + } + ] + }, + { + "name": "Admin Enable User", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/{{adminUserId}}/enable" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.isDisabled).to.eql(false);" + ] + } + } + ] + }, + { + "name": "Admin Delete User", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/{{adminUserId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.eql(\u0027User deleted successfully\u0027);" + ] + } + } + ] + }, + { + "name": "Admin Discover Users", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/users/admin/discover?page=1\u0026limit=20\u0026q={{usersQuery}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "admin", + "discover" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "limit", + "value": "20" + }, + { + "key": "q", + "value": "{{usersQuery}}" + } + ] + } + } + }, + { + "name": "Admin Get User Profile Overview", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/users/admin/{{id}}/profile-overview", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + "admin", + "{{id}}", + "profile-overview" + ] + } + } + } + ] + }, + { + "name": "Posts", + "item": [ + { + "name": "Admin Get Posts Moderation", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/posts/admin/moderation?page=1\u0026limit=20\u0026moderationStatus=flagged", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "posts", + "admin", + "moderation" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "limit", + "value": "20" + }, + { + "key": "moderationStatus", + "value": "flagged" + } + ] + } + } + } + ] + }, + { + "name": "Comments", + "item": [ + { + "name": "Admin Delete Comment", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/comments/admin/{{commentId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.success).to.eql(true);" + ] + } + } + ] + }, + { + "name": "Admin List Comments", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/comments/admin?page=1\u0026limit=20\u0026moderationStatus=flagged", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "comments", + "admin" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "limit", + "value": "20" + }, + { + "key": "moderationStatus", + "value": "flagged" + } + ] + } + } + } + ] + }, + { + "name": "Notifications", + "item": [ + { + "name": "SuperAdmin Notifications", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/notifications/superadmin?page=1\u0026limit=20\u0026type={{notificationType}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "notifications", + "superadmin" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "limit", + "value": "20" + }, + { + "key": "type", + "value": "{{notificationType}}" + } + ] + } + } + } + ] + }, + { + "name": "Smoke", + "item": [ + { + "name": "Login (Smoke)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/login", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"password\": \"StrongPass123!\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "const uid = json.user._id || json.user.id;", + "pm.environment.set(\u0027accessToken\u0027, json.accessToken);", + "pm.environment.set(\u0027refreshToken\u0027, json.refreshToken);", + "pm.environment.set(\u0027userId\u0027, uid);" + ] + } + } + ] + } + ] + }, + { + "name": "Cleanup", + "item": [ + { + "name": "Delete Instrument (if exists)", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/instruments/{{instrumentId}}" + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (!pm.environment.get(\u0027instrumentId\u0027)) { postman.setNextRequest(null); }" + ] + } + } + ] + } + ] + }, + { + "name": "Marketplace", + "item": [ + { + "name": "02 Shop Profile", + "item": [ + { + "name": "Admin", + "item": [ + { + "name": "Admin Update Shop Profile", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/shop-profile", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "shopName", + "value": "Awtarna Store", + "type": "text" + }, + { + "key": "shopDescription", + "value": "Trusted marketplace shop profile", + "type": "text" + }, + { + "key": "shopLocation", + "value": "Riyadh", + "type": "text" + }, + { + "key": "shopLatitude", + "value": "24.7136", + "type": "text" + }, + { + "key": "shopLongitude", + "value": "46.6753", + "type": "text" + }, + { + "key": "shopImageFiles", + "type": "file", + "src": [ + + ], + "disabled": true + }, + { + "key": "shopImageFiles", + "type": "file", + "src": [ + + ], + "disabled": true + } + ] + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (!pm.environment.get(\u0027adminAccessToken\u0027)) { throw new Error(\u0027Missing environment var: adminAccessToken\u0027); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.shopName).to.eql(\u0027Oudelaa Music Store\u0027);" + ] + } + } + ] + }, + { + "name": "Admin Get My Shop Profile", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/shop-profile/me" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property(\u0027shopName\u0027);" + ] + } + } + ] + } + ] + }, + { + "name": "Public", + "item": [ + { + "name": "Public Get Shop By Admin Id", + "request": { + "method": "GET", + "header": [ + + ], + "url": "{{baseUrl}}/marketplace/shops/{{adminUserId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property(\u0027shopName\u0027);" + ] + } + } + ] + } + ] + } + ] + }, + { + "name": "03 Repair Shops", + "item": [ + { + "name": "Admin", + "item": [ + { + "name": "Admin Create Repair Shop (One Shop Per Admin)", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/repair-shops", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "name", + "value": "Fix Strings Workshop", + "type": "text" + }, + { + "key": "description", + "value": "Repair shop for oud and violin", + "type": "text" + }, + { + "key": "services", + "value": "repair,setup,cleaning", + "type": "text" + }, + { + "key": "phone", + "value": "+966500000000", + "type": "text" + }, + { + "key": "whatsapp", + "value": "+966500000000", + "type": "text" + }, + { + "key": "location", + "value": "Riyadh", + "type": "text" + }, + { + "key": "latitude", + "value": "24.7136", + "type": "text" + }, + { + "key": "longitude", + "value": "46.6753", + "type": "text" + }, + { + "key": "isActive", + "value": "true", + "type": "text" + }, + { + "key": "imageFiles", + "type": "file", + "src": [ + + ], + "disabled": true + }, + { + "key": "imageFiles", + "type": "file", + "src": [ + + ], + "disabled": true + } + ] + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (!pm.environment.get(\u0027adminAccessToken\u0027)) { throw new Error(\u0027Missing environment var: adminAccessToken\u0027); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.environment.set(\u0027repairShopId\u0027, json._id || json.id);", + "pm.expect(json.name).to.eql(\u0027Oudelaa Repair Center\u0027);" + ] + } + } + ] + }, + { + "name": "Admin Create Second Repair Shop Should Fail", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/repair-shops", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "name", + "value": "Fix Strings Workshop", + "type": "text" + }, + { + "key": "description", + "value": "Repair shop for oud and violin", + "type": "text" + }, + { + "key": "services", + "value": "repair,setup,cleaning", + "type": "text" + }, + { + "key": "phone", + "value": "+966500000000", + "type": "text" + }, + { + "key": "whatsapp", + "value": "+966500000000", + "type": "text" + }, + { + "key": "location", + "value": "Riyadh", + "type": "text" + }, + { + "key": "latitude", + "value": "24.7136", + "type": "text" + }, + { + "key": "longitude", + "value": "46.6753", + "type": "text" + }, + { + "key": "isActive", + "value": "true", + "type": "text" + }, + { + "key": "imageFiles", + "type": "file", + "src": [ + + ], + "disabled": true + }, + { + "key": "imageFiles", + "type": "file", + "src": [ + + ], + "disabled": true + } + ] + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (!pm.environment.get(\u0027adminAccessToken\u0027)) { throw new Error(\u0027Missing environment var: adminAccessToken\u0027); }", + "if (!pm.environment.get(\u0027repairShopId\u0027)) { throw new Error(\u0027Create the first repair shop before running this negative test.\u0027); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 400\u0027, function () { pm.response.to.have.status(400); });", + "const json = pm.response.json();", + "const messages = Array.isArray(json.message) ? json.message : [json.message];", + "pm.expect(messages.join(\u0027 \u0027)).to.include(\u0027Each admin can create one marketplace shop only\u0027);" + ] + } + } + ] + }, + { + "name": "Admin Update Repair Shop", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/repair-shops/{{repairShopId}}", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "name", + "value": "Updated Fix Strings Workshop", + "type": "text" + }, + { + "key": "description", + "value": "Updated repair shop description", + "type": "text" + }, + { + "key": "services", + "value": "repair,setup,polishing", + "type": "text" + }, + { + "key": "phone", + "value": "+966511111111", + "type": "text" + }, + { + "key": "whatsapp", + "value": "+966511111111", + "type": "text" + }, + { + "key": "location", + "value": "Jeddah", + "type": "text" + }, + { + "key": "latitude", + "value": "21.5433", + "type": "text" + }, + { + "key": "longitude", + "value": "39.1728", + "type": "text" + }, + { + "key": "isActive", + "value": "true", + "type": "text" + }, + { + "key": "imageFiles", + "type": "file", + "src": [ + + ], + "disabled": true + }, + { + "key": "imageFiles", + "type": "file", + "src": [ + + ], + "disabled": true + } + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Admin Get My Repair Shops", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/repair-shops/me?q={{repairShopQuery}}\u0026isActive={{repairShopIsActive}}\u0026sortBy={{repairShopSortBy}}\u0026sortOrder={{listSortOrder}}\u0026page=1\u0026limit=20" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;", + "if (json.items \u0026\u0026 json.items.length \u003e 0) { pm.environment.set(\u0027repairShopId\u0027, json.items[0]._id || json.items[0].id); }" + ] + } + } + ] + }, + { + "name": "Admin Delete Repair Shop", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/repair-shops/{{repairShopId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.success).to.eql(true);" + ] + } + } + ] + } + ] + } + ] + }, + { + "name": "04 Musical Instruments", + "item": [ + { + "name": "Admin", + "item": [ + { + "name": "Admin Create Instrument", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/instruments", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "title", + "value": "Concert Guitar", + "type": "text" + }, + { + "key": "description", + "value": "Acoustic guitar in excellent condition", + "type": "text" + }, + { + "key": "price", + "value": "2100", + "type": "text" + }, + { + "key": "currency", + "value": "SAR", + "type": "text" + }, + { + "key": "quantity", + "value": "1", + "type": "text" + }, + { + "key": "condition", + "value": "used", + "type": "text" + }, + { + "key": "instrumentType", + "value": "Guitar", + "type": "text" + }, + { + "key": "imageFiles", + "type": "file", + "src": [ + + ], + "disabled": true + }, + { + "key": "imageFiles", + "type": "file", + "src": [ + + ], + "disabled": true + } + ] + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (!pm.environment.get(\u0027adminAccessToken\u0027)) { throw new Error(\u0027Missing environment var: adminAccessToken\u0027); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.environment.set(\u0027instrumentId\u0027, json._id || json.id);" + ] + } + } + ] + }, + { + "name": "Admin Update Instrument", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/instruments/{{instrumentId}}", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "title", + "value": "Updated Concert Guitar", + "type": "text" + }, + { + "key": "description", + "value": "Updated instrument description", + "type": "text" + }, + { + "key": "price", + "value": "2400", + "type": "text" + }, + { + "key": "currency", + "value": "SAR", + "type": "text" + }, + { + "key": "quantity", + "value": "1", + "type": "text" + }, + { + "key": "condition", + "value": "used", + "type": "text" + }, + { + "key": "instrumentType", + "value": "Violin", + "type": "text" + }, + { + "key": "imageFiles", + "type": "file", + "src": [ + + ], + "disabled": true + }, + { + "key": "imageFiles", + "type": "file", + "src": [ + + ], + "disabled": true + } + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Admin Get My Instruments", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/instruments/me?q={{marketplaceQuery}}\u0026minPrice={{marketplaceMinPrice}}\u0026maxPrice={{marketplaceMaxPrice}}\u0026isActive={{marketplaceIsActive}}\u0026condition={{marketplaceCondition}}\u0026instrumentType={{marketplaceInstrumentType}}\u0026sortBy={{marketplaceSortBy}}\u0026sortOrder={{listSortOrder}}\u0026page=1\u0026limit=20" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;", + "if (json.items \u0026\u0026 json.items.length \u003e 0) { pm.environment.set(\u0027instrumentId\u0027, json.items[0]._id || json.items[0].id); }" + ] + } + } + ] + }, + { + "name": "Admin Delete Instrument", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/instruments/{{instrumentId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.success).to.eql(true);" + ] + } + } + ] + } + ] + } + ] + }, + { + "name": "05 General Marketplace", + "item": [ + { + "name": "Admin", + "item": [ + { + "name": "Admin Create Listing", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/listings", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "title", + "value": "Microphone SM58", + "type": "text" + }, + { + "key": "description", + "value": "Durable dynamic microphone for studio and stage", + "type": "text" + }, + { + "key": "price", + "value": "450", + "type": "text" + }, + { + "key": "currency", + "value": "SAR", + "type": "text" + }, + { + "key": "quantity", + "value": "1", + "type": "text" + }, + { + "key": "condition", + "value": "used", + "type": "text" + }, + { + "key": "instrumentType", + "value": "Microphone", + "type": "text" + }, + { + "key": "listingCategory", + "value": "audio_gear", + "type": "text" + }, + { + "key": "isActive", + "value": "true", + "type": "text" + }, + { + "key": "imageFiles", + "type": "file", + "src": [ + + ], + "disabled": true + }, + { + "key": "imageFiles", + "type": "file", + "src": [ + + ], + "disabled": true + } + ] + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (!pm.environment.get(\u0027adminAccessToken\u0027)) { throw new Error(\u0027Missing environment var: adminAccessToken\u0027); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.environment.set(\u0027listingId\u0027, json._id || json.id);", + "pm.expect(json.listingCategory).to.eql(\u0027accessory\u0027);" + ] + } + } + ] + }, + { + "name": "Admin Update Listing", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/listings/{{listingId}}", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "title", + "value": "Updated Microphone SM58", + "type": "text" + }, + { + "key": "description", + "value": "Updated marketplace listing description", + "type": "text" + }, + { + "key": "price", + "value": "500", + "type": "text" + }, + { + "key": "currency", + "value": "SAR", + "type": "text" + }, + { + "key": "quantity", + "value": "1", + "type": "text" + }, + { + "key": "condition", + "value": "used", + "type": "text" + }, + { + "key": "instrumentType", + "value": "Microphone", + "type": "text" + }, + { + "key": "listingCategory", + "value": "audio_gear", + "type": "text" + }, + { + "key": "isActive", + "value": "true", + "type": "text" + }, + { + "key": "imageFiles", + "type": "file", + "src": [ + + ], + "disabled": true + }, + { + "key": "imageFiles", + "type": "file", + "src": [ + + ], + "disabled": true + } + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Admin Get My Listings", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/listings/me?q={{marketplaceQuery}}\u0026minPrice={{marketplaceMinPrice}}\u0026maxPrice={{marketplaceMaxPrice}}\u0026isActive={{marketplaceIsActive}}\u0026listingCategory={{marketplaceListingCategory}}\u0026condition={{marketplaceCondition}}\u0026instrumentType={{marketplaceInstrumentType}}\u0026sortBy={{marketplaceSortBy}}\u0026sortOrder={{listSortOrder}}\u0026page=1\u0026limit=20" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;", + "if (json.items \u0026\u0026 json.items.length \u003e 0) { pm.environment.set(\u0027listingId\u0027, json.items[0]._id || json.items[0].id); }" + ] + } + } + ] + }, + { + "name": "Admin Delete Listing", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/listings/{{listingId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.success).to.eql(true);" + ] + } + } + ] + } + ] + } + ] + }, + { + "name": "SuperAdmin List Marketplace Listings", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/marketplace/superadmin/listings?page=1\u0026limit=20", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "marketplace", + "superadmin", + "listings" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "limit", + "value": "20" + } + ] + } + } + }, + { + "name": "SuperAdmin Update Listing Status", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/marketplace/superadmin/listings/{{id}}/status", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "marketplace", + "superadmin", + "listings", + "{{id}}", + "status" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"isActive\": false,\n \"moderationStatus\": \"flagged\",\n \"moderationReason\": \"Needs marketplace review\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "SuperAdmin Delete Listing", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/marketplace/superadmin/listings/{{id}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "marketplace", + "superadmin", + "listings", + "{{id}}" + ] + } + } + }, + { + "name": "SuperAdmin List Repair Shops", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/marketplace/superadmin/repair-shops?page=1\u0026limit=20", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "marketplace", + "superadmin", + "repair-shops" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "limit", + "value": "20" + } + ] + } + } + }, + { + "name": "SuperAdmin Update Repair Shop Status", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/marketplace/superadmin/repair-shops/{{id}}/status", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "marketplace", + "superadmin", + "repair-shops", + "{{id}}", + "status" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"isActive\": false,\n \"moderationStatus\": \"flagged\",\n \"moderationReason\": \"Needs marketplace review\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "SuperAdmin Delete Repair Shop", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/marketplace/superadmin/repair-shops/{{id}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "marketplace", + "superadmin", + "repair-shops", + "{{id}}" + ] + } + } + }, + { + "name": "SuperAdmin Create Listing For Admin", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/marketplace/superadmin/admins/{{adminId}}/listings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "marketplace", + "superadmin", + "admins", + "{{adminId}}", + "listings" + ] + }, + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "title", + "value": "Superadmin listing", + "type": "text" + }, + { + "key": "description", + "value": "Created by superadmin", + "type": "text" + }, + { + "key": "price", + "value": "1000", + "type": "text" + }, + { + "key": "currency", + "value": "SAR", + "type": "text" + }, + { + "key": "quantity", + "value": "1", + "type": "text" + }, + { + "key": "listingCategory", + "value": "general", + "type": "text" + } + ] + } + } + }, + { + "name": "SuperAdmin Create Instrument For Admin", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/marketplace/superadmin/admins/{{adminId}}/instruments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "marketplace", + "superadmin", + "admins", + "{{adminId}}", + "instruments" + ] + }, + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "title", + "value": "Superadmin instrument", + "type": "text" + }, + { + "key": "description", + "value": "Created by superadmin", + "type": "text" + }, + { + "key": "price", + "value": "1200", + "type": "text" + }, + { + "key": "currency", + "value": "SAR", + "type": "text" + }, + { + "key": "quantity", + "value": "1", + "type": "text" + }, + { + "key": "instrumentType", + "value": "Oud", + "type": "text" + } + ] + } + } + }, + { + "name": "SuperAdmin Create Repair Shop For Admin", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/marketplace/superadmin/admins/{{adminId}}/repair-shops", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "marketplace", + "superadmin", + "admins", + "{{adminId}}", + "repair-shops" + ] + }, + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "name", + "value": "Superadmin Repair Shop", + "type": "text" + }, + { + "key": "description", + "value": "Created by superadmin", + "type": "text" + }, + { + "key": "services", + "value": "oud repair,string replacement", + "type": "text" + }, + { + "key": "location", + "value": "Riyadh", + "type": "text" + } + ] + } + } + }, + { + "name": "SuperAdmin Update Admin Shop Profile", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/marketplace/superadmin/admins/{{adminId}}/shop-profile", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "marketplace", + "superadmin", + "admins", + "{{adminId}}", + "shop-profile" + ] + }, + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "shopName", + "value": "Superadmin Shop", + "type": "text" + }, + { + "key": "shopDescription", + "value": "Updated by superadmin", + "type": "text" + }, + { + "key": "shopLocation", + "value": "Riyadh", + "type": "text" + } + ] + } + } + } + ] + }, + { + "name": "Reports", + "item": [ + { + "name": "Superadmin Reports", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/reports/superadmin?status=open" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Superadmin Update Report Status", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/reports/superadmin/{{reportId}}/status", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "reports", + "superadmin", + "{{reportId}}", + "status" + ], + "query": [ + + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"status\": \"in_review\",\n \"resolutionNote\": \"Under review\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + } + ] + }, + { + "name": "Audit", + "item": [ + { + "name": "SuperAdmin Audit Logs", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/audit/superadmin/logs?page=1\u0026limit=20\u0026action={{auditAction}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "audit", + "superadmin", + "logs" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "limit", + "value": "20" + }, + { + "key": "action", + "value": "{{auditAction}}" + } + ] + } + } + } + ] + }, + { + "name": "SuperAdmin", + "item": [ + { + "name": "Get SuperAdmin Session", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/session", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "session" + ] + } + } + }, + { + "name": "Get SuperAdmin Overview", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/overview", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "overview" + ] + } + } + }, + { + "name": "Get SuperAdmin Charts", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/charts?range=30d", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "charts" + ], + "query": [ + { + "key": "range", + "value": "30d" + } + ] + } + } + }, + { + "name": "Get SuperAdmin Recent Activity", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/recent-activity?page=1\u0026limit=20", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "recent-activity" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "limit", + "value": "20" + } + ] + } + } + }, + { + "name": "Get SuperAdmin Reports", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/reports?range=30d", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "reports" + ], + "query": [ + { + "key": "range", + "value": "30d" + } + ] + } + } + }, + { + "name": "Get SuperAdmin Ops", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/ops", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "ops" + ] + } + } + }, + { + "name": "List SuperAdmin Cases", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/cases?page=1\u0026limit=20\u0026status=open", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "cases" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "limit", + "value": "20" + }, + { + "key": "status", + "value": "open" + } + ] + } + } + }, + { + "name": "Create SuperAdmin Case", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/cases", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "cases" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Manual review\",\n \"description\": \"Created from Postman\",\n \"targetType\": \"post\",\n \"targetId\": \"{{postId}}\",\n \"priority\": \"medium\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "Update SuperAdmin Case", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/cases/{{caseId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "cases", + "{{caseId}}" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"status\": \"in_review\",\n \"resolutionNote\": \"Updated from Postman\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "Perform SuperAdmin Bulk Action", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/bulk-actions", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "bulk-actions" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"targetType\": \"post\",\n \"targetIds\": [\n \"{{bulkActionTargetId}}\"\n ],\n \"action\": \"flag\",\n \"reason\": \"Bulk moderation from Postman\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "Get SuperAdmin Settings", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/settings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "settings" + ] + } + } + }, + { + "name": "Get SuperAdmin Settings History", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/settings/history?page=1\u0026limit=20", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "settings", + "history" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "limit", + "value": "20" + } + ] + } + } + }, + { + "name": "Update SuperAdmin Settings", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/settings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "settings" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"maintenanceMode\": false\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "Restore SuperAdmin Settings Version", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/settings/history/{{historyId}}/restore", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "settings", + "history", + "{{historyId}}", + "restore" + ] + } + } + }, + { + "name": "Update Post Moderation Status", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/posts/{{postId}}/status", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "posts", + "{{postId}}", + "status" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"status\": \"flagged\",\n \"reason\": \"Needs review\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "Delete Post By SuperAdmin", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/posts/{{postId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "posts", + "{{postId}}" + ] + } + } + }, + { + "name": "Restore Post By SuperAdmin", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/posts/{{postId}}/restore", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "posts", + "{{postId}}", + "restore" + ] + } + } + }, + { + "name": "Update Comment Moderation Status", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/comments/{{commentId}}/status", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "comments", + "{{commentId}}", + "status" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"status\": \"flagged\",\n \"reason\": \"Needs review\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "Delete Comment By SuperAdmin", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/comments/{{commentId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "comments", + "{{commentId}}" + ] + } + } + }, + { + "name": "Restore Comment By SuperAdmin", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/comments/{{commentId}}/restore", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "comments", + "{{commentId}}", + "restore" + ] + } + } + }, + { + "name": "Update User Status By SuperAdmin", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/superadmin/users/{{userId}}/status", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "superadmin", + "users", + "{{userId}}", + "status" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"isDisabled\": true,\n \"reason\": \"Policy violation\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + } + ] + }, + { + "name": "oudelaa_dashboard", + "item": [ + { + "name": "Follow System", + "item": [ + { + "name": "Follow User", + "request": { + "method": "POST", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/{{targetUserId}}/follow", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Follow response\u0027, function () { const json = pm.response.json(); pm.expect(json).to.have.property(\u0027isFollowing\u0027); pm.expect(json).to.have.property(\u0027targetUserId\u0027); });" + ] + } + } + }, + { + "name": "Unfollow User", + "request": { + "method": "DELETE", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/{{targetUserId}}/follow", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Unfollow response\u0027, function () { const json = pm.response.json(); pm.expect(json.isFollowing).to.eql(false); });" + ] + } + } + }, + { + "name": "Get Follow Status", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/{{targetUserId}}/follow-status", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Follow status shape\u0027, function () { const json = pm.response.json(); pm.expect(json).to.have.property(\u0027isFollowing\u0027); pm.expect(json).to.have.property(\u0027isFollowedBy\u0027); pm.expect(json).to.have.property(\u0027isMutual\u0027); });" + ] + } + } + }, + { + "name": "Get User Followers", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/{{userId}}/followers?page=1\u0026limit=20\u0026sortOrder={{listSortOrder}}", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + }, + { + "name": "Get User Following", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/{{userId}}/following?page=1\u0026limit=20\u0026sortOrder={{listSortOrder}}", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + }, + { + "name": "Get My Followers", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/me/followers?page=1\u0026limit=20\u0026sortOrder={{listSortOrder}}", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + }, + { + "name": "Get My Following", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/me/following?page=1\u0026limit=20\u0026sortOrder={{listSortOrder}}", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + }, + { + "name": "Legacy Toggle Follow", + "request": { + "method": "POST", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/follows/toggle", + "body": { + "mode": "raw", + "raw": "{\n \"targetUserId\": \"{{targetUserId}}\"\n}" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + }, + { + "name": "List Follow Requests", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/follows/requests?page=1\u0026limit=20", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + }, + { + "name": "Approve Follow Request", + "request": { + "method": "PATCH", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/follows/requests/{{followRequestId}}/approve", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + }, + { + "name": "Reject Follow Request", + "request": { + "method": "PATCH", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/follows/requests/{{followRequestId}}/reject", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + } + ] + }, + { + "name": "Presence \u0026 Profile", + "item": [ + { + "name": "Get User Presence", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/{{userId}}/presence", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Presence shape\u0027, function () { const json = pm.response.json(); pm.expect(json).to.have.property(\u0027userId\u0027); pm.expect(json).to.have.property(\u0027isOnline\u0027); pm.expect(json).to.have.property(\u0027lastSeenAt\u0027); });" + ] + } + } + }, + { + "name": "Get User Profile With isFollowing", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/{{userId}}", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + }, + { + "name": "Get Profile Overview With Presence/Follow State", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/{{userId}}/profile-overview", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + } + ] + }, + { + "name": "User Posts", + "item": [ + { + "name": "Get Posts By User Id", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/{{userId}}/posts?page=1\u0026limit=20\u0026sortOrder={{listSortOrder}}", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Paginated posts\u0027, function () { const json = pm.response.json(); pm.expect(json).to.have.property(\u0027items\u0027); pm.expect(json).to.have.property(\u0027pagination\u0027); });" + ] + } + } + }, + { + "name": "Legacy Get Posts By User Id", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/posts/user/{{userId}}?page=1\u0026limit=20\u0026sortOrder={{listSortOrder}}", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + } + ] + }, + { + "name": "Devices FCM", + "item": [ + { + "name": "Register Device FCM Token", + "request": { + "method": "POST", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/devices/register", + "body": { + "mode": "raw", + "raw": "{\n \"fcmToken\": \"{{fcmToken}}\",\n \"platform\": \"android\",\n \"deviceId\": \"{{deviceId}}\",\n \"appVersion\": \"1.0.0\",\n \"locale\": \"ar\"\n}" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Device registered\u0027, function () { const json = pm.response.json(); pm.expect(json.message).to.eql(\u0027Device registered successfully\u0027); pm.expect(json).to.have.property(\u0027device\u0027); });" + ] + } + } + }, + { + "name": "Unregister Device FCM Token", + "request": { + "method": "POST", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/devices/unregister", + "body": { + "mode": "raw", + "raw": "{\n \"fcmToken\": \"{{fcmToken}}\"\n}" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Device unregistered\u0027, function () { const json = pm.response.json(); pm.expect(json.message).to.eql(\u0027Device unregistered successfully\u0027); });" + ] + } + } + } + ] + }, + { + "name": "Notifications", + "item": [ + { + "name": "Get My Notifications", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/notifications?read={{notificationRead}}\u0026sortOrder={{listSortOrder}}\u0026page=1\u0026limit=20\u0026type={{notificationType}}", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + }, + { + "name": "Get My Unread Notifications Count", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/notifications/unread-count", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + }, + { + "name": "Mark Notification Read", + "request": { + "method": "PATCH", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/notifications/{{notificationId}}/read", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + }, + { + "name": "Mark All Notifications Read", + "request": { + "method": "PATCH", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/notifications/read-all", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + }, + { + "name": "SuperAdmin Notifications (Dashboard Only)", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/notifications/superadmin?page=1\u0026limit=20\u0026type={{notificationType}}", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + } + ] + }, + { + "name": "Collaboration Requests", + "item": [ + { + "name": "Create Collaboration Request", + "request": { + "method": "POST", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/posts/{{postId}}/collaboration-requests", + "body": { + "mode": "raw", + "raw": "{\n \"targetUserId\": \"{{targetUserId}}\"\n}" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Collaboration request response\u0027, function () { const json = pm.response.json(); pm.expect(json).to.have.property(\u0027request\u0027); });" + ] + } + } + }, + { + "name": "List My Collaboration Requests", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/collaboration-requests?page=1\u0026limit=20", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + }, + { + "name": "Approve Collaboration Request", + "request": { + "method": "PATCH", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/collaboration-requests/{{collaborationRequestId}}/approve", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Approved\u0027, function () { const json = pm.response.json(); pm.expect(json.approved).to.eql(true); });" + ] + } + } + }, + { + "name": "Reject Collaboration Request", + "request": { + "method": "PATCH", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/collaboration-requests/{{collaborationRequestId}}/reject", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Rejected\u0027, function () { const json = pm.response.json(); pm.expect(json.rejected).to.eql(true); });" + ] + } + } + } + ] + } + ] + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:4000/api/v1", + "type": "string" + }, + { + "key": "accessToken", + "value": "", + "type": "string" + }, + { + "key": "refreshToken", + "value": "", + "type": "string" + }, + { + "key": "superAdminAccessToken", + "value": "", + "type": "string" + }, + { + "key": "superAdminRefreshToken", + "value": "", + "type": "string" + }, + { + "key": "targetAccessToken", + "value": "", + "type": "string" + }, + { + "key": "targetRefreshToken", + "value": "", + "type": "string" + }, + { + "key": "userId", + "value": "", + "type": "string" + }, + { + "key": "currentUserId", + "value": "", + "type": "string" + }, + { + "key": "targetUserId", + "value": "", + "type": "string" + }, + { + "key": "targetUsername", + "value": "", + "type": "string" + }, + { + "key": "lastFollowingState", + "value": "", + "type": "string" + }, + { + "key": "notificationUnreadCount", + "value": "", + "type": "string" + }, + { + "key": "notificationId", + "value": "", + "type": "string" + }, + { + "key": "postId", + "value": "", + "type": "string" + }, + { + "key": "ownPostId", + "value": "", + "type": "string" + }, + { + "key": "targetPostId", + "value": "", + "type": "string" + }, + { + "key": "adminPostId", + "value": "", + "type": "string" + }, + { + "key": "commentId", + "value": "", + "type": "string" + }, + { + "key": "ownCommentId", + "value": "", + "type": "string" + }, + { + "key": "replyCommentId", + "value": "", + "type": "string" + }, + { + "key": "conversationId", + "value": "", + "type": "string" + }, + { + "key": "messageId", + "value": "", + "type": "string" + }, + { + "key": "sessionJti", + "value": "", + "type": "string" + }, + { + "key": "registerEmail", + "value": "", + "type": "string" + }, + { + "key": "registerUsername", + "value": "", + "type": "string" + }, + { + "key": "adminUserId", + "value": "", + "type": "string" + }, + { + "key": "targetLoginEmail", + "value": "", + "type": "string" + }, + { + "key": "targetLoginPassword", + "value": "", + "type": "string" + }, + { + "key": "resetCode", + "value": "", + "type": "string" + }, + { + "key": "resetToken", + "value": "", + "type": "string" + }, + { + "key": "emailVerificationCode", + "value": "", + "type": "string" + }, + { + "key": "googleIdToken", + "value": "", + "type": "string" + }, + { + "key": "newPassword", + "value": "NewStrongPass123!", + "type": "string" + }, + { + "key": "feedCursor", + "value": "", + "type": "string" + }, + { + "key": "chatConversationsCursor", + "value": "", + "type": "string" + }, + { + "key": "chatMessagesCursor", + "value": "", + "type": "string" + }, + { + "key": "adminAccessToken", + "value": "", + "type": "string" + }, + { + "key": "adminRefreshToken", + "value": "", + "type": "string" + }, + { + "key": "adminEmail", + "value": "store_admin@example.com", + "type": "string" + }, + { + "key": "adminUsername", + "value": "store_admin_01", + "type": "string" + }, + { + "key": "adminPassword", + "value": "AdminStrongPass123!", + "type": "string" + }, + { + "key": "usersQuery", + "value": "user", + "type": "string" + }, + { + "key": "usersVerified", + "value": "true", + "type": "string" + }, + { + "key": "usersSortBy", + "value": "createdAt", + "type": "string" + }, + { + "key": "listSortOrder", + "value": "desc", + "type": "string" + }, + { + "key": "commentSortBy", + "value": "createdAt", + "type": "string" + }, + { + "key": "postVisibility", + "value": "public", + "type": "string" + }, + { + "key": "postSortBy", + "value": "createdAt", + "type": "string" + }, + { + "key": "postTypeFilter", + "value": "image", + "type": "string" + }, + { + "key": "postSearchQuery", + "value": "music", + "type": "string" + }, + { + "key": "postHashtag", + "value": "music", + "type": "string" + }, + { + "key": "reelQuery", + "value": "reel", + "type": "string" + }, + { + "key": "notificationRead", + "value": "false", + "type": "string" + }, + { + "key": "notificationType", + "value": "mention", + "type": "string" + }, + { + "key": "notificationResourceType", + "value": "post", + "type": "string" + }, + { + "key": "feedPreferredPostType", + "value": "video", + "type": "string" + }, + { + "key": "feedIncludeSuggestions", + "value": "true", + "type": "string" + }, + { + "key": "feedSuggestionInterval", + "value": "4", + "type": "string" + }, + { + "key": "feedFollowingOnly", + "value": "false", + "type": "string" + }, + { + "key": "feedRadiusKm", + "value": "30", + "type": "string" + }, + { + "key": "homeFeedHasMixedItems", + "value": "", + "type": "string" + }, + { + "key": "lastViewCount", + "value": "", + "type": "string" + }, + { + "key": "lastPlayCount", + "value": "", + "type": "string" + }, + { + "key": "lastShareCount", + "value": "", + "type": "string" + }, + { + "key": "marketplaceQuery", + "value": "oud", + "type": "string" + }, + { + "key": "marketplaceMinPrice", + "value": "1000", + "type": "string" + }, + { + "key": "marketplaceMaxPrice", + "value": "5000", + "type": "string" + }, + { + "key": "marketplaceIsActive", + "value": "true", + "type": "string" + }, + { + "key": "marketplaceSortBy", + "value": "price", + "type": "string" + }, + { + "key": "repairShopQuery", + "value": "riyadh", + "type": "string" + }, + { + "key": "repairShopIsActive", + "value": "true", + "type": "string" + }, + { + "key": "repairShopSortBy", + "value": "name", + "type": "string" + }, + { + "key": "instrumentId", + "value": "", + "type": "string" + }, + { + "key": "repairShopId", + "value": "", + "type": "string" + }, + { + "key": "generatedMusicUrl", + "value": "", + "type": "string" + }, + { + "key": "aiMusicPrompt", + "value": "Calm oud melody with light percussion and emotional Arabic mood", + "type": "string" + }, + { + "key": "aiMusicDuration", + "value": "12", + "type": "string" + }, + { + "key": "aiMusicSeed", + "value": "42", + "type": "string" + }, + { + "key": "listingId", + "value": "", + "type": "string" + }, + { + "key": "marketplaceListingCategory", + "value": "accessory", + "type": "string" + }, + { + "key": "marketplaceCondition", + "value": "used", + "type": "string" + }, + { + "key": "marketplaceInstrumentType", + "value": "oud", + "type": "string" + }, + { + "key": "talentRole", + "value": "instrumentalist", + "type": "string" + }, + { + "key": "talentExperienceLevel", + "value": "", + "type": "string" + }, + { + "key": "usersHasAvatar", + "value": "true", + "type": "string" + }, + { + "key": "reactionType", + "value": "love", + "type": "string" + }, + { + "key": "followRequestId", + "value": "", + "type": "string" + }, + { + "key": "reportId", + "value": "", + "type": "string" + }, + { + "key": "repostId", + "value": "", + "type": "string" + }, + { + "key": "quotePostId", + "value": "", + "type": "string" + }, + { + "key": "replyToMessageId", + "value": "" + }, + { + "key": "mediaFilePath", + "value": "" + }, + { + "key": "collaboratorId", + "value": "" + }, + { + "key": "commentKeyword", + "value": "spam" + }, + { + "key": "reportReason", + "value": "spam" + }, + { + "key": "sessionId", + "value": "" + }, + { + "key": "caseId", + "value": "" + }, + { + "key": "historyId", + "value": "" + }, + { + "key": "bulkActionTargetId", + "value": "" + }, + { + "key": "auditAction", + "value": "" + } + ] +} \ No newline at end of file diff --git a/postman/Oudelaa-Mobile.postman_collection.json b/postman/Oudelaa-Mobile.postman_collection.json index 469ac22..0e54214 100644 --- a/postman/Oudelaa-Mobile.postman_collection.json +++ b/postman/Oudelaa-Mobile.postman_collection.json @@ -1,5898 +1,6252 @@ { - "info": { - "name": "Oudelaa Mobile API", - "description": "Mobile/API app collection generated from Oudelaa master collection.", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "Auth", - "item": [ - { - "name": "Register Basic", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/register-basic", - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"password\": \"StrongPass123!\",\n \"confirmPassword\": \"StrongPass123!\"\n}" - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const ts = Date.now();", - "if (!pm.environment.get('registerEmail')) { pm.environment.set('registerEmail', `test_${ts}@example.com`); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.expect(json.message).to.exist;", - "pm.expect(json.email).to.exist;", - "if (json.debugCode) { pm.environment.set('emailVerificationCode', json.debugCode); }" - ] - } - } - ] - }, - { - "name": "Register", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/register", - "body": { - "mode": "raw", - "raw": "{\n \"name\": \"Test User\",\n \"stageName\": \"Artist One\",\n \"username\": \"{{registerUsername}}\",\n \"email\": \"{{registerEmail}}\",\n \"password\": \"StrongPass123!\",\n \"confirmPassword\": \"StrongPass123!\",\n \"musicRoles\": [\"singer\", \"composer\"],\n \"musicGenres\": [\"Tarab\", \"Pop\"],\n \"favoriteInstruments\": [\"Oud\", \"Piano\"],\n \"favoriteMaqamat\": [\"Bayati\", \"Rast\"],\n \"location\": \"Riyadh, Saudi Arabia\",\n \"latitude\": 24.7136,\n \"longitude\": 46.6753,\n \"isPrivate\": false\n}" - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const ts = Date.now();", - "if (!pm.environment.get('registerEmail')) { pm.environment.set('registerEmail', `test_${ts}@example.com`); }", - "if (!pm.environment.get('registerUsername')) { pm.environment.set('registerUsername', `test_user_${ts}`); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.expect(json.message).to.exist;", - "pm.expect(json.email).to.exist;", - "if (json.debugCode) { pm.environment.set('emailVerificationCode', json.debugCode); }" - ] - } - } - ] - }, - { - "name": "Login", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/login", - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"password\": \"StrongPass123!\"\n}" - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const missing = [];", - "if (!pm.environment.get('accessToken')) missing.push('accessToken');", - "if (!pm.environment.get('postId')) missing.push('postId');", - "if (missing.length) { throw new Error('Missing environment vars: ' + missing.join(', ')); }" - ] - } - }, - { - "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.user).to.exist;", - "const uid = json.user._id || json.user.id;", - "pm.environment.set('accessToken', json.accessToken);", - "pm.environment.set('refreshToken', json.refreshToken);", - "pm.environment.set('userId', uid);", - "pm.environment.set('currentUserId', uid);" - ] - } - } - ] - }, - { - "name": "Target User Login", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/login", - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"{{targetLoginEmail}}\",\n \"password\": \"{{targetLoginPassword}}\"\n}" - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const missing = [];", - "if (!pm.environment.get('accessToken')) missing.push('accessToken');", - "if (!pm.environment.get('postId')) missing.push('postId');", - "if (missing.length) { throw new Error('Missing environment vars: ' + missing.join(', ')); }" - ] - } - }, - { - "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.user).to.exist;", - "const uid = json.user._id || json.user.id;", - "pm.environment.set('targetAccessToken', json.accessToken);", - "pm.environment.set('targetRefreshToken', json.refreshToken);", - "pm.environment.set('targetUserId', uid);" - ] - } - } - ] - }, - { - "name": "Admin Login", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/login", - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"{{adminEmail}}\",\n \"password\": \"{{adminPassword}}\"\n}" - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const missing = [];", - "if (!pm.environment.get('accessToken')) missing.push('accessToken');", - "if (!pm.environment.get('postId')) missing.push('postId');", - "if (missing.length) { throw new Error('Missing environment vars: ' + missing.join(', ')); }" - ] - } - }, - { - "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.environment.set('adminAccessToken', json.accessToken);", - "pm.environment.set('adminRefreshToken', json.refreshToken);", - "pm.environment.set('adminUserId', (json.user && (json.user._id || json.user.id)) || pm.environment.get('adminUserId'));" - ] - } - } - ] - }, - { - "name": "Refresh", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/refresh", - "body": { - "mode": "raw", - "raw": "{\n \"refreshToken\": \"{{refreshToken}}\"\n}" - } - }, - "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.user).to.exist;", - "pm.environment.set('accessToken', json.accessToken);", - "pm.environment.set('refreshToken', json.refreshToken);", - "pm.environment.set('userId', json.user._id || json.user.id);" - ] - } - } - ] - }, - { - "name": "Logout", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/logout", - "body": { - "mode": "raw", - "raw": "{\n \"refreshToken\": \"{{refreshToken}}\"\n}" - } - }, - "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.message).to.eql('Logged out successfully');" - ] - } - } - ] - }, - { - "name": "Google OAuth Start (Browser)", - "request": { - "method": "GET", - "header": [], - "url": "{{baseUrl}}/auth/google", - "description": "Open this request in browser flow to start Google OAuth redirect." - } - }, - { - "name": "Google OAuth Callback (Browser Redirect)", - "request": { - "method": "GET", - "header": [], - "url": "{{baseUrl}}/auth/google/callback", - "description": "Callback endpoint is called by Google redirect after browser login. Do not call directly from Postman." - } - }, - { - "name": "Google Token Login (Mobile - Recommended)", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/google/token", - "description": "Mobile-style Google login (Instagram-like): send Google idToken from app SDK.", - "body": { - "mode": "raw", - "raw": "{\n \"idToken\": \"{{googleIdToken}}\"\n}" - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const idToken = (pm.environment.get('googleIdToken') || '').trim();", - "if (!idToken) {", - " throw new Error('googleIdToken is empty. Paste Google idToken from Flutter/Google SDK into environment variable googleIdToken.');", - "}" - ] - } - }, - { - "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.accessToken).to.exist;", - "pm.expect(json.refreshToken).to.exist;", - "pm.expect(json.user).to.exist;", - "const uid = json.user._id || json.user.id;", - "pm.environment.set('accessToken', json.accessToken);", - "pm.environment.set('refreshToken', json.refreshToken);", - "pm.environment.set('userId', uid);", - "pm.environment.set('currentUserId', uid);" - ] - } - } - ] - }, - { - "name": "List My Sessions", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/auth/sessions" - }, - "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.pagination).to.exist;", - "if (json.items && json.items.length > 0) { pm.environment.set('sessionJti', json.items[0].jti); }" - ] - } - } - ] - }, - { - "name": "Revoke My Session", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/auth/sessions/{{sessionJti}}/revoke" - }, - "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.success).to.eql(true);" - ] - } - } - ] - }, - { - "name": "Forgot Password", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/forgot-password", - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"{{registerEmail}}\"\n}" - } - }, - "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.message).to.exist;", - "if (json.debugCode) { pm.environment.set('resetCode', json.debugCode); }" - ] - } - } - ] - }, - { - "name": "Verify Reset Code", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/verify-reset-code", - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"code\": \"{{resetCode}}\"\n}" - } - }, - "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.resetToken).to.exist;", - "pm.environment.set('resetToken', json.resetToken);" - ] - } - } - ] - }, - { - "name": "Reset Password", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/reset-password", - "body": { - "mode": "raw", - "raw": "{\n \"resetToken\": \"{{resetToken}}\",\n \"newPassword\": \"{{newPassword}}\",\n \"confirmPassword\": \"{{newPassword}}\"\n}" - } - }, - "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.message).to.eql('Password reset successfully');" - ] - } - } - ] - }, - { - "name": "Send Email Verification", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/send-email-verification", - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"{{registerEmail}}\"\n}" - } - }, - "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.message).to.exist;", - "if (json.debugCode) { pm.environment.set('emailVerificationCode', json.debugCode); }" - ] - } - } - ] - }, - { - "name": "Verify Email", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/verify-email", - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"code\": \"{{emailVerificationCode}}\"\n}" - } - }, - "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.message).to.exist;", - "if (json.accessToken) { pm.environment.set('accessToken', json.accessToken); }", - "if (json.refreshToken) { pm.environment.set('refreshToken', json.refreshToken); }", - "if (json.user) { const uid = json.user._id || json.user.id; pm.environment.set('userId', uid); pm.environment.set('currentUserId', uid); }" - ] - } - } - ] - } - ] - }, - { - "name": "Media", - "item": [ - { - "name": "Media Health", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/media/health" - }, - "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.storage).to.be.an('object');", - "pm.expect(json.storage.provider).to.be.oneOf(['local', 's3']);", - "pm.expect(json.serving.rangeRequests).to.eql(true);", - "pm.expect(json.processing).to.be.an('object');" - ] - } - } - ] - } - ] - }, - { - "name": "Users", - "item": [ - { - "name": "Search Users", - "request": { - "method": "GET", - "url": "{{baseUrl}}/users?q={{usersQuery}}&isVerified={{usersVerified}}&musicRole={{talentRole}}&experienceLevel={{talentExperienceLevel}}&hasAvatar={{usersHasAvatar}}&sortBy={{usersSortBy}}&sortOrder={{listSortOrder}}&page=1&limit=20", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ] - }, - "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.pagination).to.exist;", - "const currentUserId = pm.environment.get('currentUserId');", - "if (json.items && json.items.length > 0) {", - " const target = json.items.find(u => (u._id || u.id) !== currentUserId) || json.items[0];", - " const targetId = target._id || target.id;", - " pm.environment.set('targetUserId', targetId);", - " if (target.username) { pm.environment.set('targetUsername', target.username); }", - "}" - ] - } - } - ] - }, - { - "name": "Profile Setup", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/users/me/profile-setup", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "stageName", - "value": "Artist One", - "type": "text" - }, - { - "key": "avatarFile", - "type": "file", - "src": [] - }, - { - "key": "bio", - "value": "Short bio", - "type": "text" - }, - { - "key": "location", - "value": "Riyadh, Saudi Arabia", - "type": "text" - }, - { - "key": "latitude", - "value": "24.7136", - "type": "text" - }, - { - "key": "coverImageFile", - "type": "file", - "src": [], - "disabled": true - }, - { - "key": "longitude", - "value": "46.6753", - "type": "text" - } - ] - } - }, - "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.location).to.eql('Riyadh, Saudi Arabia');", - "pm.expect(json.latitude).to.eql(24.7136);", - "pm.expect(json.longitude).to.eql(46.6753);", - "if (json.avatar) { pm.expect(json.avatar).to.be.a('string'); }", - "if (json.coverImage) { pm.expect(json.coverImage).to.be.a('string'); }" - ] - } - } - ] - }, - { - "name": "Music Setup", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/users/me/music-setup", - "body": { - "mode": "raw", - "raw": "{\n \"musicRoles\": [\"instrumentalist\", \"composer\"],\n \"musicGenres\": [\"Tarab\", \"Pop\"],\n \"experienceLevel\": \"intermediate\",\n \"favoriteInstruments\": [\"Oud\", \"Piano\"],\n \"favoriteMaqamat\": [\"Bayati\", \"Rast\"]\n}" - } - }, - "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.musicRoles).to.include('instrumentalist');", - "pm.expect(json.musicRoles).to.include('composer');", - "pm.expect(json.experienceLevel).to.eql('intermediate');", - "pm.expect(json).to.not.have.property('isInstrumentalist');", - "pm.expect(json).to.not.have.property('isSinger');", - "pm.expect(json).to.not.have.property('isComposer');", - "pm.expect(json).to.not.have.property('isLyricist');" - ] - } - } - ] - }, - { - "name": "Update Me", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/users/me", - "body": { - "mode": "raw", - "raw": "{\n \"bio\": \"Updated from Postman\",\n \"avatar\": \"https://cdn.example.com/avatar.jpg\",\n \"stageName\": \"Artist One Updated\",\n \"musicGenres\": [\"Tarab\", \"Khaleeji\"],\n \"favoriteInstruments\": [\"Oud\"],\n \"favoriteMaqamat\": [\"Hijaz\"],\n \"location\": \"Jeddah, Saudi Arabia\"\n}" - } - }, - "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.bio).to.eql('Updated from Postman');", - "if (json.coverImage) { pm.expect(json.coverImage).to.be.a('string'); }" - ] - } - } - ] - }, - { - "name": "Update Me With Files", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/users/me", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "stageName", - "value": "Artist One Files", - "type": "text" - }, - { - "key": "bio", - "value": "Updated from Postman with files", - "type": "text" - }, - { - "key": "location", - "value": "Riyadh, Saudi Arabia", - "type": "text" - }, - { - "key": "avatar", - "value": "https://cdn.example.com/avatar.jpg", - "type": "text" - }, - { - "key": "avatarFile", - "type": "file", - "src": [] - }, - { - "key": "coverImageFile", - "type": "file", - "src": [], - "disabled": true - } - ] - } - }, - "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.stageName).to.eql('Artist One Files');", - "if (json.avatar) { pm.expect(json.avatar).to.be.a('string'); }", - "if (json.coverImage) { pm.expect(json.coverImage).to.be.a('string'); }" - ] - } - } - ] - }, - { - "name": "Get User By Id", - "request": { - "method": "GET", - "url": "{{baseUrl}}/users/{{userId}}", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ] - } - }, - { - "name": "Discover Talents", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/users/discover?q={{usersQuery}}&musicRole={{talentRole}}&experienceLevel={{talentExperienceLevel}}&hasAvatarOnly={{usersHasAvatar}}&includeRoleBuckets=true&sortBy=followersCount&sortOrder={{listSortOrder}}&page=1&limit=8" - }, - "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('roleBuckets');", - "pm.expect(json.pagination).to.exist;", - "if (json.items && json.items.length > 0) { pm.environment.set('targetUserId', json.items[0]._id || json.items[0].id); }" - ] - } - } - ] - }, - { - "name": "Get Profile Overview", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/users/{{targetUserId}}/profile-overview" - }, - "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('stats');", - "pm.expect(json).to.have.property('contentCounts');", - "pm.expect(json).to.have.property('viewerState');" - ] - } - } - ] - } - ] - }, - { - "name": "Posts", - "item": [ - { - "name": "Create Post (Own)", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "content", - "value": "First text post with mention @{{targetUsername}} #music", - "type": "text" - }, - { - "key": "taggedUserIds", - "value": "{{targetUserId}}", - "type": "text" - }, - { - "key": "mentionUsernames", - "value": "{{targetUsername}}", - "type": "text" - }, - { - "key": "location", - "value": "Riyadh, Saudi Arabia", - "type": "text" - }, - { - "key": "latitude", - "value": "24.7136", - "type": "text" - }, - { - "key": "longitude", - "value": "46.6753", - "type": "text" - }, - { - "key": "visibility", - "value": "public", - "type": "text" - } - ] - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "if (!pm.environment.get('accessToken')) { throw new Error('Missing environment var: accessToken'); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "const pid = json._id || json.id;", - "pm.environment.set('postId', pid);", - "pm.environment.set('ownPostId', pid);" - ] - } - } - ] - }, - { - "name": "Create Carousel Post (Instagram Style)", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "content", - "value": "Weekend jam session #oud #music", - "type": "text" - }, - { - "key": "visibility", - "value": "public", - "type": "text" - }, - { - "key": "taggedUserIds", - "value": "{{targetUserId}}", - "type": "text" - }, - { - "key": "mentionUsernames", - "value": "{{targetUsername}}", - "type": "text" - }, - { - "key": "location", - "value": "Riyadh, Saudi Arabia", - "type": "text" - }, - { - "key": "latitude", - "value": "24.7136", - "type": "text" - }, - { - "key": "longitude", - "value": "46.6753", - "type": "text" - }, - { - "key": "imageFiles", - "type": "file", - "src": [] - }, - { - "key": "imageFiles", - "type": "file", - "src": [] - } - ] - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "if (!pm.environment.get('accessToken')) { throw new Error('Missing environment var: accessToken'); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.expect(json.postType).to.eql('image');", - "pm.expect(json.processingStatus).to.eql('ready');", - "pm.expect(json.media).to.be.an('object');", - "pm.expect(json.media.mediaType).to.eql(json.postType);", - "pm.expect(json.imageUrls).to.be.an('array');", - "pm.expect(json.media.images).to.be.an('array').that.is.not.empty;", - "pm.expect(json.media.images[0].url).to.be.a('string');", - "pm.expect(json.imageUrls.length).to.be.greaterThan(0);", - "const pid = json._id || json.id;", - "pm.environment.set('postId', pid);", - "pm.environment.set('ownPostId', pid);" - ] - } - } - ] - }, - { - "name": "Update Carousel Post (Instagram Style)", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts/{{postId}}", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "content", - "value": "Updated carousel caption #oud #live", - "type": "text" - }, - { - "key": "imageFiles", - "type": "file", - "src": [] - } - ] - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const missing = [];", - "if (!pm.environment.get('accessToken')) missing.push('accessToken');", - "if (!pm.environment.get('postId')) missing.push('postId');", - "if (missing.length) { throw new Error('Missing environment vars: ' + missing.join(', ')); }" - ] - } - }, - { - "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.postType).to.eql('image');", - "pm.expect(json.imageUrls).to.be.an('array');" - ] - } - } - ] - }, - { - "name": "Get User Posts (Own)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/posts/user/{{userId}}?visibility={{postVisibility}}&sortBy={{postSortBy}}&sortOrder={{listSortOrder}}&page=1&limit=10", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ] - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const missing = [];", - "if (!pm.environment.get('accessToken')) missing.push('accessToken');", - "if (!pm.environment.get('userId')) missing.push('userId');", - "if (missing.length) { throw new Error('Missing environment vars: ' + missing.join(', ')); }" - ] - } - }, - { - "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.pagination).to.exist;", - "if (!pm.environment.get('postId') && json.items && json.items.length > 0) {", - " pm.environment.set('postId', json.items[0]._id || json.items[0].id);", - " pm.environment.set('ownPostId', json.items[0]._id || json.items[0].id);", - "}" - ] - } - } - ] - }, - { - "name": "Get User Posts Filtered", - "request": { - "method": "GET", - "url": "{{baseUrl}}/posts/user/{{userId}}?visibility={{postVisibility}}&postType={{postTypeFilter}}&q={{postSearchQuery}}&hashtag={{postHashtag}}&sortBy={{postSortBy}}&sortOrder={{listSortOrder}}&page=1&limit=10", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ] - }, - "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.pagination).to.exist;" - ] - } - } - ] - }, - { - "name": "Get Post By Id", - "request": { - "method": "GET", - "url": "{{baseUrl}}/posts/{{postId}}", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ] - }, - "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._id || json.id).to.exist;", - "pm.expect(json).to.have.property('content');" - ] - } - } - ] - }, - { - "name": "Register Post View", - "request": { - "method": "POST", - "url": "{{baseUrl}}/posts/{{postId}}/view", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ] - }, - "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.success).to.eql(true);", - "pm.expect(json).to.have.property('viewCount');", - "pm.environment.set('lastViewCount', json.viewCount);" - ] - } - } - ] - }, - { - "name": "Register Post Share", - "request": { - "method": "POST", - "url": "{{baseUrl}}/posts/{{postId}}/share", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ] - }, - "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.success).to.eql(true);", - "pm.expect(json).to.have.property('shareCount');", - "pm.environment.set('lastShareCount', json.shareCount);" - ] - } - } - ] - }, - { - "name": "Update Post", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts/{{postId}}", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "content", - "value": "Updated post content with @{{targetUsername}} #live", - "type": "text" - }, - { - "key": "taggedUserIds", - "value": "{{targetUserId}}", - "type": "text" - }, - { - "key": "mentionUsernames", - "value": "{{targetUsername}}", - "type": "text" - }, - { - "key": "location", - "value": "Jeddah, Saudi Arabia", - "type": "text" - }, - { - "key": "latitude", - "value": "21.5433", - "type": "text" - }, - { - "key": "longitude", - "value": "39.1728", - "type": "text" - } - ] - } - }, - "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.content).to.eql('Updated post content with @artist_one #live');" - ] - } - } - ] - }, - { - "name": "Create Video Post (Own)", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "content", - "value": "Video post #music", - "type": "text" - }, - { - "key": "visibility", - "value": "public", - "type": "text" - }, - { - "key": "durationSeconds", - "value": "42", - "type": "text" - }, - { - "key": "coverImageFile", - "type": "file", - "src": [] - }, - { - "key": "style", - "value": "Sharqi", - "type": "text" - }, - { - "key": "maqam", - "value": "Hijaz", - "type": "text" - }, - { - "key": "mentionUsernames", - "value": "{{targetUsername}}", - "type": "text" - }, - { - "key": "rhythmSignature", - "value": "6/8", - "type": "text" - }, - { - "key": "location", - "value": "Riyadh, Saudi Arabia", - "type": "text" - }, - { - "key": "latitude", - "value": "24.7136", - "type": "text" - }, - { - "key": "longitude", - "value": "46.6753", - "type": "text" - }, - { - "key": "videoFile", - "type": "file", - "src": [] - } - ] - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "if (!pm.environment.get('accessToken')) { throw new Error('Missing environment var: accessToken'); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.expect(json.postType).to.eql('video');", - "pm.expect(json.media.thumbnailUrl).to.be.a('string');", - "pm.expect(json.media.preferredPlaybackUrl).to.be.a('string');", - "if (json.hlsUrl) { pm.expect(json.media.preferredPlaybackUrl).to.eql(json.hlsUrl); }", - "pm.expect(json.processingStatus).to.eql('ready');", - "pm.expect(json.media).to.be.an('object');", - "pm.expect(json.media.mediaType).to.eql(json.postType);", - "pm.expect(json.durationSeconds).to.eql(42);", - "if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a('string'); }", - "pm.expect(json.style).to.eql('Sharqi');", - "pm.expect(json.maqam).to.eql('Hijaz');", - "pm.expect(json.rhythmSignature).to.eql('6/8');", - "pm.environment.set('postId', json._id || json.id);", - "pm.environment.set('ownPostId', json._id || json.id);" - ] - } - } - ] - }, - { - "name": "Create Reel (Video First)", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts/reels", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "content", - "value": "New reel from oud session #reel", - "type": "text" - }, - { - "key": "visibility", - "value": "public", - "type": "text" - }, - { - "key": "durationSeconds", - "value": "30", - "type": "text" - }, - { - "key": "coverImageFile", - "type": "file", - "src": [] - }, - { - "key": "style", - "value": "Sharqi", - "type": "text" - }, - { - "key": "maqam", - "value": "Hijaz", - "type": "text" - }, - { - "key": "rhythmSignature", - "value": "6/8", - "type": "text" - }, - { - "key": "videoFile", - "type": "file", - "src": [] - } - ] - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "if (!pm.environment.get('accessToken')) { throw new Error('Missing environment var: accessToken'); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.expect(json.postType).to.eql('video');", - "pm.expect(json.media.thumbnailUrl).to.be.a('string');", - "pm.expect(json.media.preferredPlaybackUrl).to.be.a('string');", - "if (json.hlsUrl) { pm.expect(json.media.preferredPlaybackUrl).to.eql(json.hlsUrl); }", - "pm.expect(json.processingStatus).to.eql('ready');", - "pm.expect(json.media).to.be.an('object');", - "pm.expect(json.media.mediaType).to.eql(json.postType);", - "pm.expect(json.durationSeconds).to.eql(30);", - "if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a('string'); }", - "pm.expect(json.style).to.eql('Sharqi');", - "pm.expect(json.maqam).to.eql('Hijaz');", - "pm.expect(json.rhythmSignature).to.eql('6/8');", - "pm.environment.set('postId', json._id || json.id);", - "pm.environment.set('ownPostId', json._id || json.id);" - ] - } - } - ] - }, - { - "name": "Get Reels", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts/reels?visibility={{postVisibility}}&authorId={{userId}}&sortBy={{postSortBy}}&sortOrder={{listSortOrder}}&page=1&limit=10" - }, - "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.pagination).to.exist;" - ] - } - } - ] - }, - { - "name": "Get Reels Filtered", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts/reels?visibility={{postVisibility}}&authorId={{userId}}&q={{reelQuery}}&sortBy={{postSortBy}}&sortOrder={{listSortOrder}}&page=1&limit=10" - }, - "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.pagination).to.exist;" - ] - } - } - ] - }, - { - "name": "Create Audio Post (Own)", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "content", - "value": "Audio post #oud", - "type": "text" - }, - { - "key": "visibility", - "value": "public", - "type": "text" - }, - { - "key": "durationSeconds", - "value": "54", - "type": "text" - }, - { - "key": "coverImageFile", - "type": "file", - "src": [] - }, - { - "key": "style", - "value": "Sharqi", - "type": "text" - }, - { - "key": "maqam", - "value": "Hijaz", - "type": "text" - }, - { - "key": "rhythmSignature", - "value": "6/8", - "type": "text" - }, - { - "key": "audioFile", - "type": "file", - "src": [] - } - ] - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "if (!pm.environment.get('accessToken')) { throw new Error('Missing environment var: accessToken'); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.expect(json.postType).to.eql('audio');", - "pm.expect(json.media.audioUrl).to.be.a('string');", - "pm.expect(json.media.waveformPeaks).to.be.an('array');", - "pm.expect(json.media.durationSeconds).to.exist;", - "pm.expect(json.processingStatus).to.eql('ready');", - "pm.expect(json.media).to.be.an('object');", - "pm.expect(json.media.mediaType).to.eql(json.postType);", - "pm.expect(json.durationSeconds).to.eql(54);", - "if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a('string'); }", - "pm.expect(json.style).to.eql('Sharqi');", - "pm.expect(json.maqam).to.eql('Hijaz');", - "pm.expect(json.rhythmSignature).to.eql('6/8');", - "pm.environment.set('postId', json._id || json.id);", - "pm.environment.set('ownPostId', json._id || json.id);" - ] - } - } - ] - }, - { - "name": "Create Audio Post (URL + Waveform)", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "content", - "value": "Audio post with waveform #oud #hijaz", - "type": "text" - }, - { - "key": "audioUrl", - "value": "https://cdn.example.com/audio-sample.mp3", - "type": "text" - }, - { - "key": "durationSeconds", - "value": "54", - "type": "text" - }, - { - "key": "thumbnailUrl", - "value": "https://cdn.example.com/audio-cover.jpg", - "type": "text" - }, - { - "key": "style", - "value": "Sharqi", - "type": "text" - }, - { - "key": "maqam", - "value": "Hijaz", - "type": "text" - }, - { - "key": "rhythmSignature", - "value": "6/8", - "type": "text" - }, - { - "key": "waveformPeaks", - "value": "[12,38,27,49,22,44]", - "type": "text" - }, - { - "key": "visibility", - "value": "public", - "type": "text" - } - ] - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "if (!pm.environment.get('accessToken')) { throw new Error('Missing environment var: accessToken'); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.expect(json.postType).to.eql('audio');", - "pm.expect(json.media.audioUrl).to.be.a('string');", - "pm.expect(json.media.waveformPeaks).to.be.an('array');", - "pm.expect(json.media.durationSeconds).to.exist;", - "pm.expect(json.processingStatus).to.eql('ready');", - "pm.expect(json.media).to.be.an('object');", - "pm.expect(json.media.mediaType).to.eql(json.postType);", - "pm.expect(json.waveformPeaks).to.be.an('array');", - "pm.expect(json.waveformPeaks.length).to.eql(6);", - "pm.expect(json.maqam).to.eql('Hijaz');", - "pm.environment.set('postId', json._id || json.id);", - "pm.environment.set('ownPostId', json._id || json.id);" - ] - } - } - ] - }, - { - "name": "Register Post Play (Audio/Video)", - "request": { - "method": "POST", - "url": "{{baseUrl}}/posts/{{postId}}/play", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ] - }, - "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.success).to.eql(true);", - "pm.expect(json).to.have.property('playCount');", - "pm.environment.set('lastPlayCount', json.playCount);" - ] - } - } - ] - }, - { - "name": "Create Invalid Post (Video+Audio)", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "content", - "value": "Invalid post", - "type": "text" - }, - { - "key": "videoUrl", - "value": "https://cdn.example.com/video.mp4", - "type": "text" - }, - { - "key": "audioUrl", - "value": "https://cdn.example.com/audio.mp3", - "type": "text" - }, - { - "key": "visibility", - "value": "public", - "type": "text" - } - ] - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "if (!pm.environment.get('accessToken')) { throw new Error('Missing environment var: accessToken'); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 400', function () { pm.response.to.have.status(400); });" - ] - } - } - ] - }, - { - "name": "Update Post Video (Form-Data)", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts/{{postId}}", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "content", - "value": "Updated with video file", - "type": "text" - }, - { - "key": "durationSeconds", - "value": "58", - "type": "text" - }, - { - "key": "coverImageFile", - "type": "file", - "src": [] - }, - { - "key": "style", - "value": "Contemporary", - "type": "text" - }, - { - "key": "maqam", - "value": "Bayati", - "type": "text" - }, - { - "key": "rhythmSignature", - "value": "4/4", - "type": "text" - }, - { - "key": "videoFile", - "type": "file", - "src": [] - } - ] - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const missing = [];", - "if (!pm.environment.get('accessToken')) missing.push('accessToken');", - "if (!pm.environment.get('postId')) missing.push('postId');", - "if (missing.length) { throw new Error('Missing environment vars: ' + missing.join(', ')); }" - ] - } - }, - { - "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.postType).to.eql('video');", - "pm.expect(json.durationSeconds).to.eql(58);", - "if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a('string'); }", - "pm.expect(json.style).to.eql('Contemporary');", - "pm.expect(json.maqam).to.eql('Bayati');", - "pm.expect(json.rhythmSignature).to.eql('4/4');" - ] - } - } - ] - }, - { - "name": "Update Post Audio (Form-Data)", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts/{{postId}}", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "content", - "value": "Updated with audio file", - "type": "text" - }, - { - "key": "durationSeconds", - "value": "61", - "type": "text" - }, - { - "key": "coverImageFile", - "type": "file", - "src": [] - }, - { - "key": "style", - "value": "Tarab", - "type": "text" - }, - { - "key": "maqam", - "value": "Nahawand", - "type": "text" - }, - { - "key": "rhythmSignature", - "value": "6/8", - "type": "text" - }, - { - "key": "audioFile", - "type": "file", - "src": [] - } - ] - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const missing = [];", - "if (!pm.environment.get('accessToken')) missing.push('accessToken');", - "if (!pm.environment.get('postId')) missing.push('postId');", - "if (missing.length) { throw new Error('Missing environment vars: ' + missing.join(', ')); }" - ] - } - }, - { - "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.postType).to.eql('audio');", - "pm.expect(json.durationSeconds).to.eql(61);", - "if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a('string'); }", - "pm.expect(json.style).to.eql('Tarab');", - "pm.expect(json.maqam).to.eql('Nahawand');", - "pm.expect(json.rhythmSignature).to.eql('6/8');" - ] - } - } - ] - }, - { - "name": "Delete Post (Cleanup - Run Last)", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts/{{postId}}" - }, - "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.success).to.eql(true);" - ] - } - } - ] - }, - { - "name": "Repost Post", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/posts/{{postId}}/repost", - "body": { - "mode": "raw", - "raw": "{\n \"content\": \"\"\n}" - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.environment.set('repostId', json._id || json.id);" - ] - } - } - ] - }, - { - "name": "Quote Post", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/posts/{{postId}}/repost", - "body": { - "mode": "raw", - "raw": "{\n \"content\": \"My take on this post\"\n}" - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.environment.set('quotePostId', json._id || json.id);" - ] - } - } - ] - }, - { - "name": "Update Post Comment Settings", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/posts/{{postId}}/comment-settings", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "posts", - "{{postId}}", - "comment-settings" - ], - "query": [] - }, - "body": { - "mode": "raw", - "raw": "{\n \"commentsDisabled\": false,\n \"commentsFollowersOnly\": true,\n \"commentFilterKeywords\": [\n \"{{commentKeyword}}\",\n \"offensive\"\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "Pin Post To Profile", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/posts/{{postId}}/pin-profile", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "posts", - "{{postId}}", - "pin-profile" - ], - "query": [] - } - } - }, - { - "name": "Unpin Post From Profile", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/posts/{{postId}}/unpin-profile", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "posts", - "{{postId}}", - "unpin-profile" - ], - "query": [] - } - } - }, - { - "name": "Archive Post", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/posts/{{postId}}/archive", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "posts", - "{{postId}}", - "archive" - ], - "query": [] - } - } - }, - { - "name": "Restore Archived Post", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/posts/{{postId}}/restore-archive", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "posts", - "{{postId}}", - "restore-archive" - ], - "query": [] - } - } - }, - { - "name": "Create Carousel Post With Metadata", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/posts", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "posts" - ], - "query": [] - }, - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "content", - "value": "Carousel caption #oud", - "type": "text" - }, - { - "key": "imageUrls", - "value": "https://cdn.example.com/1.jpg,https://cdn.example.com/2.jpg", - "type": "text" - }, - { - "key": "imageCaptions", - "value": "First image,Second image", - "type": "text" - }, - { - "key": "imageAltTexts", - "value": "Oud on stage,Audience closeup", - "type": "text" - }, - { - "key": "collaboratorIds", - "value": "{{collaboratorId}}", - "type": "text" - }, - { - "key": "commentsDisabled", - "value": "false", - "type": "text" - }, - { - "key": "commentsFollowersOnly", - "value": "false", - "type": "text" - } - ] - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test(\"Request succeeded\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201]); });", - "const json = pm.response.json();", - "const value = json.item;", - "if (value) pm.environment.set(\"postId\", value._id || value.id || value);" - ] - } - } - ] - }, - { - "name": "Update Carousel Alt Text", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/posts/{{postId}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "posts", - "{{postId}}" - ], - "query": [] - }, - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "imageUrls", - "value": "https://cdn.example.com/1.jpg,https://cdn.example.com/2.jpg", - "type": "text" - }, - { - "key": "imageCaptions", - "value": "Updated first,Updated second", - "type": "text" - }, - { - "key": "imageAltTexts", - "value": "Updated oud alt,Updated audience alt", - "type": "text" - } - ] - } - } - }, - { - "name": "Disable Comments On Post", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/posts/{{postId}}/comment-settings", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "posts", - "{{postId}}", - "comment-settings" - ], - "query": [] - }, - "body": { - "mode": "raw", - "raw": "{\n \"commentsDisabled\": true,\n \"commentsFollowersOnly\": false,\n \"commentFilterKeywords\": []\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "Enable Comments On Post", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/posts/{{postId}}/comment-settings", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "posts", - "{{postId}}", - "comment-settings" - ], - "query": [] - }, - "body": { - "mode": "raw", - "raw": "{\n \"commentsDisabled\": false,\n \"commentsFollowersOnly\": false,\n \"commentFilterKeywords\": []\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - } - ] - }, - { - "name": "Comments", - "item": [ - { - "name": "Create Comment", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/comments", - "body": { - "mode": "raw", - "raw": "{\n \"postId\": \"{{postId}}\",\n \"content\": \"Awesome post!\"\n}" - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const missing = [];", - "if (!pm.environment.get('accessToken')) missing.push('accessToken');", - "if (!pm.environment.get('postId')) missing.push('postId');", - "if (missing.length) { throw new Error('Missing environment vars: ' + missing.join(', ')); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "const cid = json._id || json.id;", - "pm.environment.set('commentId', cid);", - "pm.environment.set('ownCommentId', cid);" - ] - } - } - ] - }, - { - "name": "Create Comment With Mention", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/comments", - "body": { - "mode": "raw", - "raw": "{\n \"postId\": \"{{postId}}\",\n \"content\": \"Awesome post @{{targetUsername}}!\",\n \"mentionUsernames\": [\"{{targetUsername}}\"]\n}" - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const missing = [];", - "if (!pm.environment.get('accessToken')) missing.push('accessToken');", - "if (!pm.environment.get('postId')) missing.push('postId');", - "if (!pm.environment.get('targetUsername')) missing.push('targetUsername');", - "if (missing.length) { throw new Error('Missing environment vars: ' + missing.join(', ')); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "const cid = json._id || json.id;", - "pm.environment.set('commentId', cid);", - "pm.expect(json.mentionUsernames || []).to.include(pm.environment.get('targetUsername').toLowerCase());" - ] - } - } - ] - }, - { - "name": "Get Post Comments", - "request": { - "method": "GET", - "url": "{{baseUrl}}/comments/post/{{postId}}?page=1&limit=10&sortBy={{commentSortBy}}&sortOrder={{listSortOrder}}", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ] - }, - "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.pagination).to.exist;" - ] - } - } - ] - }, - { - "name": "Create Comment Reply", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/comments/{{commentId}}/replies", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "comments", - "{{commentId}}", - "replies" - ], - "query": [] - }, - "body": { - "mode": "raw", - "raw": "{\n \"content\": \"Reply to comment\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test(\"Request succeeded\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201]); });", - "const json = pm.response.json();", - "const value = json.item;", - "if (value) pm.environment.set(\"replyCommentId\", value._id || value.id || value);" - ] - } - } - ] - }, - { - "name": "Get Comment Replies", - "request": { - "method": "GET", - "url": "{{baseUrl}}/comments/{{commentId}}/replies?page=1&limit=10&sortBy={{commentSortBy}}&sortOrder={{listSortOrder}}", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ] - }, - "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.pagination).to.exist;" - ] - } - } - ] - }, - { - "name": "Update Comment", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/comments/{{commentId}}", - "body": { - "mode": "raw", - "raw": "{\n \"content\": \"Edited comment @{{secondaryUsername}}\",\n \"mentionUsernames\": [\"{{secondaryUsername}}\"]\n}" - } - }, - "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.content).to.include('Edited comment');" - ] - } - } - ] - }, - { - "name": "Delete Comment", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/comments/{{commentId}}" - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" - ] - } - } - ] - }, - { - "name": "Pin Comment", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/comments/{{commentId}}/pin", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "comments", - "{{commentId}}", - "pin" - ], - "query": [] - } - } - }, - { - "name": "Unpin Comment", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/comments/{{commentId}}/unpin", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "comments", - "{{commentId}}", - "unpin" - ], - "query": [] - } - } - }, - { - "name": "Create Comment Hidden By Filter", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/comments", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "comments" - ], - "query": [] - }, - "body": { - "mode": "raw", - "raw": "{\n \"postId\": \"{{postId}}\",\n \"content\": \"This contains {{commentKeyword}} keyword\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "Get Post Comments - Top", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/comments/post/{{postId}}?page=1&limit=20&sortBy=top&sortOrder=desc", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "comments", - "post", - "{{postId}}" - ], - "query": [ - { - "key": "page", - "value": "1" - }, - { - "key": "limit", - "value": "20" - }, - { - "key": "sortBy", - "value": "top" - }, - { - "key": "sortOrder", - "value": "desc" - } - ] - } - } - }, - { - "name": "Get Post Comments - Newest", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/comments/post/{{postId}}?page=1&limit=20&sortBy=createdAt&sortOrder=desc", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "comments", - "post", - "{{postId}}" - ], - "query": [ - { - "key": "page", - "value": "1" - }, - { - "key": "limit", - "value": "20" - }, - { - "key": "sortBy", - "value": "createdAt" - }, - { - "key": "sortOrder", - "value": "desc" - } - ] - } - } - } - ] - }, - { - "name": "Likes", - "item": [ - { - "name": "Like Post (Set liked=true)", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/likes", - "body": { - "mode": "raw", - "raw": "{\n \"targetId\": \"{{postId}}\",\n \"targetType\": \"post\"\n}" - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const missing = [];", - "if (!pm.environment.get('accessToken')) missing.push('accessToken');", - "if (!pm.environment.get('postId')) missing.push('postId');", - "if (missing.length) { throw new Error('Missing environment vars: ' + missing.join(', ')); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.expect(json.liked).to.eql(true);" - ] - } - } - ] - }, - { - "name": "Unlike Post (Set liked=false)", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/likes/post/{{postId}}" - }, - "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.liked).to.eql(false);" - ] - } - } - ] - }, - { - "name": "Get Post Like Status", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/likes/status/post/{{postId}}" - }, - "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('liked');" - ] - } - } - ] - }, - { - "name": "Like Comment", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/likes", - "body": { - "mode": "raw", - "raw": "{\n \"targetId\": \"{{commentId}}\",\n \"targetType\": \"comment\"\n}" - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.expect(json.liked).to.eql(true);" - ] - } - } - ] - }, - { - "name": "Unlike Comment", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/likes/comment/{{commentId}}" - }, - "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.liked).to.eql(false);" - ] - } - } - ] - }, - { - "name": "Get Comment Like Status", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/likes/status/comment/{{commentId}}" - }, - "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('liked');" - ] - } - } - ] - }, - { - "name": "React To Post", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/likes", - "body": { - "mode": "raw", - "raw": "{\n \"targetId\": \"{{postId}}\",\n \"targetType\": \"post\",\n \"reactionType\": \"{{reactionType}}\"\n}" - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.expect(json.reactionType).to.exist;" - ] - } - } - ] - }, - { - "name": "Reaction Status - Post", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/likes/status/post/{{postId}}" - }, - "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('reactionSummary');" - ] - } - } - ] - }, - { - "name": "Toggle Like", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/likes/toggle", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "likes", - "toggle" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"targetType\": \"post\",\n \"targetId\": \"{{postId}}\",\n \"reactionType\": \"like\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - } - ] - }, - { - "name": "Saves", - "item": [ - { - "name": "Save Post (Set saved=true)", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/saves", - "body": { - "mode": "raw", - "raw": "{\n \"postId\": \"{{postId}}\"\n}" - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const missing = [];", - "if (!pm.environment.get('accessToken')) missing.push('accessToken');", - "if (!pm.environment.get('postId')) missing.push('postId');", - "if (missing.length) { throw new Error('Missing environment vars: ' + missing.join(', ')); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.expect(json.saved).to.eql(true);" - ] - } - } - ] - }, - { - "name": "Unsave Post (Set saved=false)", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/saves/{{postId}}" - }, - "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.saved).to.eql(false);" - ] - } - } - ] - }, - { - "name": "Get Save Status", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/saves/status/{{postId}}" - }, - "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('saved');" - ] - } - } - ] - }, - { - "name": "Get My Saved Posts", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/saves/me?page=1&limit=10&sortOrder={{listSortOrder}}" - }, - "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.pagination).to.exist;" - ] - } - } - ] - }, - { - "name": "Toggle Save", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/saves/toggle", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "saves", - "toggle" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"postId\": \"{{postId}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - } - ] - }, - { - "name": "Follows", - "item": [ - { - "name": "Toggle Follow User", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/follows/toggle", - "body": { - "mode": "raw", - "raw": "{\n \"targetUserId\": \"{{targetUserId}}\"\n}" - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const missing = [];", - "if (!pm.environment.get('accessToken')) missing.push('accessToken');", - "if (!pm.environment.get('targetUserId')) missing.push('targetUserId');", - "if (missing.length) { throw new Error('Missing environment vars: ' + missing.join(', ')); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.expect(json).to.have.property('following');", - "pm.environment.set('lastFollowingState', json.following ? 'following' : 'not_following');" - ] - } - } - ] - }, - { - "name": "Get Followers", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/follows/followers/{{userId}}?page=1&limit=10&sortOrder={{listSortOrder}}" - }, - "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.pagination).to.exist;" - ] - } - } - ] - }, - { - "name": "Get Following", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/follows/following/{{userId}}?page=1&limit=10&sortOrder={{listSortOrder}}" - }, - "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.pagination).to.exist;" - ] - } - } - ] - }, - { - "name": "Get Follow Status", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/follows/status/{{targetUserId}}" - }, - "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('following');" - ] - } - } - ] - }, - { - "name": "Get Follow Suggestions", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/follows/suggestions?page=1&limit=10&sortOrder={{listSortOrder}}" - }, - "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.pagination).to.exist;" - ] - } - } - ] - }, - { - "name": "List Follow Requests", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/follows/requests?page=1&limit=20" - }, - "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();", - "if (json.items && json.items.length) { pm.environment.set('followRequestId', json.items[0]._id || json.items[0].id); }" - ] - } - } - ] - }, - { - "name": "Approve Follow Request", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/follows/requests/{{followRequestId}}/approve" - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" - ] - } - } - ] - }, - { - "name": "Reject Follow Request", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/follows/requests/{{followRequestId}}/reject" - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" - ] - } - } - ] - } - ] - }, - { - "name": "Notifications", - "item": [ - { - "name": "Get My Notifications", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/notifications?read={{notificationRead}}&sortOrder={{listSortOrder}}&page=1&limit=20" - }, - "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.pagination).to.exist;", - "if (json.items && json.items.length > 0) {", - " pm.environment.set('notificationId', json.items[0]._id || json.items[0].id);", - " pm.expect(json.items[0]).to.have.property('title');", - " pm.expect(json.items[0]).to.have.property('resourceType');", - " pm.expect(json.items[0]).to.have.property('deepLink');", - "}" - ] - } - } - ] - }, - { - "name": "Get My Notifications Filtered", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/notifications?read={{notificationRead}}&type={{notificationType}}&resourceType={{notificationResourceType}}&sortOrder={{listSortOrder}}&page=1&limit=20" - }, - "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.pagination).to.exist;" - ] - } - } - ] - }, - { - "name": "Get Target User Notifications", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{targetAccessToken}}" - } - ], - "url": "{{baseUrl}}/notifications?page=1&limit=20&read=false&sortOrder={{listSortOrder}}" - }, - "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.pagination).to.exist;", - "pm.environment.set('notificationUnreadCount', json.unreadCount || 0);", - "const followNotification = (json.items || []).find(item => item.type === 'follow');", - "if (followNotification) {", - " pm.environment.set('notificationId', followNotification._id || followNotification.id);", - " pm.expect(followNotification).to.have.property('title');", - " pm.expect(followNotification).to.have.property('resourceType');", - " pm.expect(followNotification).to.have.property('deepLink');", - "}" - ] - } - } - ] - }, - { - "name": "Get Unread Notifications Count", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/notifications/unread-count" - }, - "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('unreadCount');" - ] - } - } - ] - }, - { - "name": "Get Target User Unread Notifications Count", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{targetAccessToken}}" - } - ], - "url": "{{baseUrl}}/notifications/unread-count" - }, - "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('unreadCount');", - "pm.environment.set('notificationUnreadCount', json.unreadCount || 0);" - ] - } - } - ] - }, - { - "name": "Mark Notification Read", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/notifications/{{notificationId}}/read" - }, - "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('unreadCount');" - ] - } - } - ] - }, - { - "name": "Mark Target Notification Read", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{targetAccessToken}}" - } - ], - "url": "{{baseUrl}}/notifications/{{notificationId}}/read" - }, - "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('unreadCount');", - "pm.environment.set('notificationUnreadCount', json.unreadCount || 0);" - ] - } - } - ] - }, - { - "name": "Mark All Notifications Read", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/notifications/read-all" - }, - "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.unreadCount).to.eql(0);" - ] - } - } - ] - }, - { - "name": "Mark All Target Notifications Read", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{targetAccessToken}}" - } - ], - "url": "{{baseUrl}}/notifications/read-all" - }, - "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.unreadCount).to.eql(0);", - "pm.environment.set('notificationUnreadCount', 0);" - ] - } - } - ] - } - ] - }, - { - "name": "Chat", - "item": [ - { - "name": "Create Conversation", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/chat/conversations", - "body": { - "mode": "raw", - "raw": "{\n \"participantIds\": [\"{{targetUserId}}\"],\n \"isGroup\": false\n}" - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const missing = [];", - "if (!pm.environment.get('accessToken')) missing.push('accessToken');", - "if (!pm.environment.get('targetUserId')) missing.push('targetUserId');", - "if (missing.length) { throw new Error('Missing environment vars: ' + missing.join(', ')); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.environment.set('conversationId', json._id || json.id);" - ] - } - } - ] - }, - { - "name": "Get My Conversations", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/chat/conversations?limit=20&cursor={{chatConversationsCursor}}&sortOrder={{listSortOrder}}" - }, - "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.pagination).to.exist;", - "if (json.items && json.items.length > 0) { pm.environment.set('conversationId', json.items[0]._id || json.items[0].id); }", - "pm.environment.set('chatConversationsCursor', json.nextCursor || '');" - ] - } - } - ] - }, - { - "name": "Send Message", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/chat/messages", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "chat", - "messages" - ], - "query": [] - }, - "body": { - "mode": "raw", - "raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello from Postman\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test(\"Request succeeded\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201]); });", - "const json = pm.response.json();", - "const value = json.item;", - "if (value) pm.environment.set(\"messageId\", value._id || value.id || value);" - ] - } - } - ] - }, - { - "name": "Get Conversation Messages", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/chat/conversations/{{conversationId}}/messages?limit=20&cursor={{chatMessagesCursor}}&sortOrder={{listSortOrder}}" - }, - "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.pagination).to.exist;", - "pm.environment.set('chatMessagesCursor', json.nextCursor || '');" - ] - } - } - ] - }, - { - "name": "Mark Message Seen", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/chat/messages/{{messageId}}/seen" - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" - ] - } - } - ] - }, - { - "name": "Unsend Message", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/chat/messages/{{messageId}}/unsend" - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" - ] - } - } - ] - }, - { - "name": "Block User In Chat", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/chat/blocks/{{targetUserId}}" - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.expect(json.blocked).to.eql(true);" - ] - } - } - ] - }, - { - "name": "Unblock User In Chat", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/chat/blocks/{{targetUserId}}/unblock" - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" - ] - } - } - ] - }, - { - "name": "Get Chat Block Status", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/chat/blocks/status/{{targetUserId}}" - }, - "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('iBlocked');", - "pm.expect(json).to.have.property('blockedMe');" - ] - } - } - ] - }, - { - "name": "Get My Chat Blocks", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/chat/blocks" - }, - "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');" - ] - } - } - ] - }, - { - "name": "Send Reply Message", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/chat/messages", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "chat", - "messages" - ], - "query": [] - }, - "body": { - "mode": "raw", - "raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Replying to a message\",\n \"replyToMessageId\": \"{{replyToMessageId}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test(\"Request succeeded\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201]); });", - "const json = pm.response.json();", - "const value = json.item;", - "if (value) pm.environment.set(\"messageId\", value._id || value.id || value);" - ] - } - } - ] - }, - { - "name": "Send Message Media Upload", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": { - "raw": "{{baseUrl}}/chat/messages/upload", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "chat", - "messages", - "upload" - ], - "query": [] - }, - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "conversationId", - "value": "{{conversationId}}", - "type": "text" - }, - { - "key": "content", - "value": "Media message caption", - "type": "text" - }, - { - "key": "replyToMessageId", - "value": "{{replyToMessageId}}", - "type": "text" - }, - { - "key": "mediaFile", - "type": "file", - "src": "{{mediaFilePath}}" - } - ] - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test(\"Request succeeded\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201]); });", - "const json = pm.response.json();", - "const value = json.item;", - "if (value) pm.environment.set(\"messageId\", value._id || value.id || value);" - ] - } - } - ] - }, - { - "name": "React To Message", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/chat/messages/{{messageId}}/reaction", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "chat", - "messages", - "{{messageId}}", - "reaction" - ], - "query": [] - }, - "body": { - "mode": "raw", - "raw": "{\n \"reactionType\": \"love\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "Delete Message For Me", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/chat/messages/{{messageId}}/delete-for-me", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "chat", - "messages", - "{{messageId}}", - "delete-for-me" - ], - "query": [] - } - } - } - ] - }, - { - "name": "Feed", - "item": [ - { - "name": "Get My Feed Mixed Home", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/feed/me?page=1&limit=20&includeSuggestions={{feedIncludeSuggestions}}&suggestionInterval={{feedSuggestionInterval}}&followingOnly={{feedFollowingOnly}}&radiusKm={{feedRadiusKm}}" - }, - "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');", - "if (json.items && json.items.length > 0) {", - " pm.expect(json.items[0]).to.have.property('feedItemType');", - "}", - "const hasMixedItems = (json.items || []).some(item => item.feedItemType === 'suggested_users' || item.feedItemType === 'featured_marketplace');", - "pm.environment.set('homeFeedHasMixedItems', hasMixedItems ? 'true' : 'false');" - ] - } - } - ] - }, - { - "name": "Get My Feed", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/feed/me?limit=20&cursor={{feedCursor}}&followingOnly={{feedFollowingOnly}}&radiusKm={{feedRadiusKm}}" - }, - "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.pagination).to.exist;", - "const firstPost = (json.items || []).find(item => item.feedItemType === 'post');", - "if (firstPost) {", - " pm.expect(firstPost).to.have.property('likedByMe');", - " pm.expect(firstPost).to.have.property('savedByMe');", - " pm.expect(firstPost).to.have.property('engagement');", - "}", - "pm.environment.set('feedCursor', json.nextCursor || '');" - ] - } - } - ] - }, - { - "name": "Get My Feed Preferred Type", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/feed/me?limit=20&cursor={{feedCursor}}&preferredPostType={{feedPreferredPostType}}&followingOnly={{feedFollowingOnly}}&radiusKm={{feedRadiusKm}}" - }, - "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.pagination).to.exist;", - "const firstPost = (json.items || []).find(item => item.feedItemType === 'post');", - "if (firstPost) { pm.expect(firstPost).to.have.property('engagement'); }", - "pm.environment.set('feedCursor', json.nextCursor || '');" - ] - } - } - ] - }, - { - "name": "Get Trending Feed", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/feed/trending?limit=20&cursor={{feedCursor}}&preferredPostType={{feedPreferredPostType}}" - }, - "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.pagination).to.exist;", - "const firstPost = (json.items || []).find(item => item.feedItemType === 'post');", - "if (firstPost) {", - " pm.expect(firstPost).to.have.property('likedByMe');", - " pm.expect(firstPost).to.have.property('savedByMe');", - "}", - "pm.environment.set('feedCursor', json.nextCursor || '');" - ] - } - } - ] - }, - { - "name": "Explore Feed", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/feed/explore?page=1&limit=20" - }, - "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');" - ] - } - } - ] - } - ] - }, - { - "name": "Smoke", - "item": [ - { - "name": "Login (Smoke)", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": "{{baseUrl}}/auth/login", - "body": { - "mode": "raw", - "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"password\": \"StrongPass123!\"\n}" - } - }, - "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();", - "const uid = json.user._id || json.user.id;", - "pm.environment.set('accessToken', json.accessToken);", - "pm.environment.set('refreshToken', json.refreshToken);", - "pm.environment.set('userId', uid);" - ] - } - } - ] - }, - { - "name": "Get User Posts (Smoke)", - "request": { - "method": "GET", - "url": "{{baseUrl}}/posts/user/{{userId}}?page=1&limit=10", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ] - } - }, - { - "name": "Get My Feed (Smoke)", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/feed/me?limit=20&cursor={{feedCursor}}&radiusKm=30" - } - } - ] - }, - { - "name": "Full E2E", - "item": [ - { - "name": "Create Post (Own) - E2E", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "content", - "value": "E2E text post", - "type": "text" - }, - { - "key": "visibility", - "value": "public", - "type": "text" - } - ] - } - } - }, - { - "name": "Create Comment - E2E", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/comments", - "body": { - "mode": "raw", - "raw": "{\n \"postId\": \"{{postId}}\",\n \"content\": \"E2E comment\"\n}" - } - } - }, - { - "name": "Like Post - E2E", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/likes", - "body": { - "mode": "raw", - "raw": "{\n \"targetId\": \"{{postId}}\",\n \"targetType\": \"post\"\n}" - } - } - }, - { - "name": "Save Post - E2E", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/saves", - "body": { - "mode": "raw", - "raw": "{\n \"postId\": \"{{postId}}\"\n}" - } - } - }, - { - "name": "Delete Post - E2E Cleanup", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts/{{postId}}" - } - } - ] - }, - { - "name": "Negative Tests", - "item": [ - { - "name": "Create Invalid Post (Video+Audio) - Negative", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "content", - "value": "Invalid post", - "type": "text" - }, - { - "key": "videoUrl", - "value": "https://cdn.example.com/video.mp4", - "type": "text" - }, - { - "key": "audioUrl", - "value": "https://cdn.example.com/audio.mp3", - "type": "text" - }, - { - "key": "visibility", - "value": "public", - "type": "text" - } - ] - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 400', function () { pm.response.to.have.status(400); });" - ] - } - } - ] - }, - { - "name": "Create Post Without Token - Negative", - "request": { - "method": "POST", - "header": [], - "url": "{{baseUrl}}/posts", - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "content", - "value": "No token request", - "type": "text" - }, - { - "key": "visibility", - "value": "public", - "type": "text" - } - ] - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 401', function () { pm.response.to.have.status(401); });" - ] - } - } - ] - } - ] - }, - { - "name": "Cleanup", - "item": [ - { - "name": "Delete Own Comment (if exists)", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/comments/{{ownCommentId}}" - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "if (!pm.environment.get('ownCommentId')) { postman.setNextRequest('Delete Own Post (if exists)'); }" - ] - } - } - ] - }, - { - "name": "Delete Own Post (if exists)", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/posts/{{ownPostId}}" - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "if (!pm.environment.get('ownPostId')) { postman.setNextRequest('Delete Instrument (if exists)'); }" - ] - } - } - ] - } - ] - }, - { - "name": "AI Music", - "item": [ - { - "name": "Generate Music From Text (Gemini)", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/media/ai/text-to-music", - "body": { - "mode": "raw", - "raw": "{\n \"prompt\": \"{{aiMusicPrompt}}\",\n \"durationSeconds\": {{aiMusicDuration}},\n \"seed\": {{aiMusicSeed}}\n}" - } - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "if (!pm.environment.get('aiMusicPrompt')) { pm.environment.set('aiMusicPrompt', 'Calm oud melody with light percussion and emotional Arabic mood'); }", - "if (!pm.environment.get('aiMusicDuration')) { pm.environment.set('aiMusicDuration', '12'); }", - "if (!pm.environment.get('aiMusicSeed')) { pm.environment.set('aiMusicSeed', '42'); }" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", - "const json = pm.response.json();", - "pm.expect(json.audioUrl).to.exist;", - "pm.expect(json.mimeType).to.exist;", - "pm.expect(json.sizeBytes).to.be.a('number');", - "pm.environment.set('generatedMusicUrl', json.audioUrl);" - ] - } - } - ] - } - ] - }, - { - "name": "Marketplace", - "item": [ - { - "name": "01 Marketplace Overview", - "item": [ - { - "name": "Public Marketplace Home", - "request": { - "method": "GET", - "header": [], - "url": "{{baseUrl}}/marketplace/home?listingsLimit=4&instrumentsLimit=4&repairShopsLimit=3&onlyActive=true" - }, - "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('categories');", - "pm.expect(json).to.have.property('summary');", - "pm.expect(json).to.have.property('featuredShops');", - "pm.expect(json.filters).to.have.property('listingCategories');", - "pm.expect(json.sections).to.have.property('listings');", - "pm.expect(json.sections).to.have.property('musicalInstruments');", - "pm.expect(json.sections).to.have.property('repairShops');" - ] - } - } - ] - } - ] - }, - { - "name": "03 Repair Shops", - "item": [ - { - "name": "Public", - "item": [ - { - "name": "Public List Repair Shops", - "request": { - "method": "GET", - "header": [], - "url": "{{baseUrl}}/marketplace/repair-shops?q={{repairShopQuery}}&isActive={{repairShopIsActive}}&sortBy={{repairShopSortBy}}&sortOrder={{listSortOrder}}&page=1&limit=20" - }, - "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.pagination).to.exist;", - "if (json.items && json.items.length > 0) { pm.environment.set('repairShopId', json.items[0]._id || json.items[0].id); }" - ] - } - } - ] - }, - { - "name": "Public Get Repair Shop By Id", - "request": { - "method": "GET", - "header": [], - "url": "{{baseUrl}}/marketplace/repair-shops/{{repairShopId}}" - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" - ] - } - } - ] - } - ] - } - ] - }, - { - "name": "04 Musical Instruments", - "item": [ - { - "name": "Public", - "item": [ - { - "name": "Public List Instruments", - "request": { - "method": "GET", - "header": [], - "url": "{{baseUrl}}/marketplace/instruments?q={{marketplaceQuery}}&minPrice={{marketplaceMinPrice}}&maxPrice={{marketplaceMaxPrice}}&isActive={{marketplaceIsActive}}&condition={{marketplaceCondition}}&instrumentType={{marketplaceInstrumentType}}&sortBy={{marketplaceSortBy}}&sortOrder={{listSortOrder}}&page=1&limit=20" - }, - "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.pagination).to.exist;", - "if (json.items && json.items.length > 0) { pm.environment.set('instrumentId', json.items[0]._id || json.items[0].id); }" - ] - } - } - ] - }, - { - "name": "Public Get Instrument By Id", - "request": { - "method": "GET", - "header": [], - "url": "{{baseUrl}}/marketplace/instruments/{{instrumentId}}" - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" - ] - } - } - ] - } - ] - } - ] - }, - { - "name": "05 General Marketplace", - "item": [ - { - "name": "Public", - "item": [ - { - "name": "Public List Listings", - "request": { - "method": "GET", - "header": [], - "url": "{{baseUrl}}/marketplace/listings?q={{marketplaceQuery}}&minPrice={{marketplaceMinPrice}}&maxPrice={{marketplaceMaxPrice}}&isActive={{marketplaceIsActive}}&listingCategory={{marketplaceListingCategory}}&condition={{marketplaceCondition}}&instrumentType={{marketplaceInstrumentType}}&sortBy={{marketplaceSortBy}}&sortOrder={{listSortOrder}}&page=1&limit=20" - }, - "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.pagination).to.exist;", - "if (json.items && json.items.length > 0) { pm.environment.set('listingId', json.items[0]._id || json.items[0].id); }" - ] - } - } - ] - }, - { - "name": "Public Get Listing By Id", - "request": { - "method": "GET", - "header": [], - "url": "{{baseUrl}}/marketplace/listings/{{listingId}}" - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" - ] - } - } - ] - } - ] - } - ] - } - ] - }, - { - "name": "Blocks", - "item": [ - { - "name": "Block User", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/blocks/{{targetUserId}}" - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 201', function () { pm.response.to.have.status(201); });" - ] - } - } - ] - }, - { - "name": "Block Status", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/blocks/status/{{targetUserId}}" - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" - ] - } - } - ] - }, - { - "name": "Unblock User", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/blocks/{{targetUserId}}/unblock" - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" - ] - } - } - ] - } - ] - }, - { - "name": "Reports", - "item": [ - { - "name": "Report Post", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/reports", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "reports" - ], - "query": [] - }, - "body": { - "mode": "raw", - "raw": "{\n \"targetType\": \"post\",\n \"targetId\": \"{{postId}}\",\n \"reason\": \"spam\",\n \"details\": \"This content looks suspicious\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test(\"Request succeeded\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201]); });", - "const json = pm.response.json();", - "const value = json.item;", - "if (value) pm.environment.set(\"reportId\", value._id || value.id || value);" - ] - } - } - ] - }, - { - "name": "My Reports", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - } - ], - "url": "{{baseUrl}}/reports/me?page=1&limit=20" - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" - ] - } - } - ] - }, - { - "name": "Report Post - Fixed Reason", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/reports", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "reports" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"targetType\": \"post\",\n \"targetId\": \"{{postId}}\",\n \"reason\": \"{{reportReason}}\",\n \"details\": \"Reported from Postman\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - } - }, - { - "name": "Report User And Block", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/reports", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "reports" - ], - "query": [] - }, - "body": { - "mode": "raw", - "raw": "{\n \"targetType\": \"user\",\n \"targetId\": \"{{targetUserId}}\",\n \"reason\": \"harassment\",\n \"details\": \"Unsafe interaction\",\n \"blockTarget\": true\n}", - "options": { - "raw": { - "language": "json" - } - } - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test(\"Request succeeded\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201]); });", - "const json = pm.response.json();", - "const value = json.item;", - "if (value) pm.environment.set(\"reportId\", value._id || value.id || value);" - ] - } - } - ] - }, - { - "name": "Report Comment", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{accessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/reports", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "reports" - ], - "query": [] - }, - "body": { - "mode": "raw", - "raw": "{\n \"targetType\": \"comment\",\n \"targetId\": \"{{commentId}}\",\n \"reason\": \"harassment\",\n \"details\": \"Offensive comment\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - }, - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test(\"Request succeeded\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201]); });", - "const json = pm.response.json();", - "const value = json.item;", - "if (value) pm.environment.set(\"reportId\", value._id || value.id || value);" - ] - } - } - ] - } - ] - }, - { - "name": "Health", - "item": [ - { - "name": "Health Check", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{baseUrl}}/", - "host": [ - "{{baseUrl}}" - ], - "path": [] - } - } - } - ] - } - ], - "variable": [ - { - "key": "baseUrl", - "value": "http://localhost:4000/api/v1", - "type": "string" - }, - { - "key": "accessToken", - "value": "", - "type": "string" - }, - { - "key": "refreshToken", - "value": "", - "type": "string" - }, - { - "key": "superAdminAccessToken", - "value": "", - "type": "string" - }, - { - "key": "superAdminRefreshToken", - "value": "", - "type": "string" - }, - { - "key": "targetAccessToken", - "value": "", - "type": "string" - }, - { - "key": "targetRefreshToken", - "value": "", - "type": "string" - }, - { - "key": "userId", - "value": "", - "type": "string" - }, - { - "key": "currentUserId", - "value": "", - "type": "string" - }, - { - "key": "targetUserId", - "value": "", - "type": "string" - }, - { - "key": "targetUsername", - "value": "", - "type": "string" - }, - { - "key": "lastFollowingState", - "value": "", - "type": "string" - }, - { - "key": "notificationUnreadCount", - "value": "", - "type": "string" - }, - { - "key": "notificationId", - "value": "", - "type": "string" - }, - { - "key": "postId", - "value": "", - "type": "string" - }, - { - "key": "ownPostId", - "value": "", - "type": "string" - }, - { - "key": "targetPostId", - "value": "", - "type": "string" - }, - { - "key": "adminPostId", - "value": "", - "type": "string" - }, - { - "key": "commentId", - "value": "", - "type": "string" - }, - { - "key": "ownCommentId", - "value": "", - "type": "string" - }, - { - "key": "replyCommentId", - "value": "", - "type": "string" - }, - { - "key": "conversationId", - "value": "", - "type": "string" - }, - { - "key": "messageId", - "value": "", - "type": "string" - }, - { - "key": "sessionJti", - "value": "", - "type": "string" - }, - { - "key": "registerEmail", - "value": "", - "type": "string" - }, - { - "key": "registerUsername", - "value": "", - "type": "string" - }, - { - "key": "adminUserId", - "value": "", - "type": "string" - }, - { - "key": "targetLoginEmail", - "value": "", - "type": "string" - }, - { - "key": "targetLoginPassword", - "value": "", - "type": "string" - }, - { - "key": "resetCode", - "value": "", - "type": "string" - }, - { - "key": "resetToken", - "value": "", - "type": "string" - }, - { - "key": "emailVerificationCode", - "value": "", - "type": "string" - }, - { - "key": "googleIdToken", - "value": "", - "type": "string" - }, - { - "key": "newPassword", - "value": "NewStrongPass123!", - "type": "string" - }, - { - "key": "feedCursor", - "value": "", - "type": "string" - }, - { - "key": "chatConversationsCursor", - "value": "", - "type": "string" - }, - { - "key": "chatMessagesCursor", - "value": "", - "type": "string" - }, - { - "key": "adminAccessToken", - "value": "", - "type": "string" - }, - { - "key": "adminRefreshToken", - "value": "", - "type": "string" - }, - { - "key": "adminEmail", - "value": "store_admin@example.com", - "type": "string" - }, - { - "key": "adminUsername", - "value": "store_admin_01", - "type": "string" - }, - { - "key": "adminPassword", - "value": "AdminStrongPass123!", - "type": "string" - }, - { - "key": "usersQuery", - "value": "user", - "type": "string" - }, - { - "key": "usersVerified", - "value": "true", - "type": "string" - }, - { - "key": "usersSortBy", - "value": "createdAt", - "type": "string" - }, - { - "key": "listSortOrder", - "value": "desc", - "type": "string" - }, - { - "key": "commentSortBy", - "value": "createdAt", - "type": "string" - }, - { - "key": "postVisibility", - "value": "public", - "type": "string" - }, - { - "key": "postSortBy", - "value": "createdAt", - "type": "string" - }, - { - "key": "postTypeFilter", - "value": "image", - "type": "string" - }, - { - "key": "postSearchQuery", - "value": "music", - "type": "string" - }, - { - "key": "postHashtag", - "value": "music", - "type": "string" - }, - { - "key": "reelQuery", - "value": "reel", - "type": "string" - }, - { - "key": "notificationRead", - "value": "false", - "type": "string" - }, - { - "key": "notificationType", - "value": "mention", - "type": "string" - }, - { - "key": "notificationResourceType", - "value": "post", - "type": "string" - }, - { - "key": "feedPreferredPostType", - "value": "video", - "type": "string" - }, - { - "key": "feedIncludeSuggestions", - "value": "true", - "type": "string" - }, - { - "key": "feedSuggestionInterval", - "value": "4", - "type": "string" - }, - { - "key": "feedFollowingOnly", - "value": "false", - "type": "string" - }, - { - "key": "feedRadiusKm", - "value": "30", - "type": "string" - }, - { - "key": "homeFeedHasMixedItems", - "value": "", - "type": "string" - }, - { - "key": "lastViewCount", - "value": "", - "type": "string" - }, - { - "key": "lastPlayCount", - "value": "", - "type": "string" - }, - { - "key": "lastShareCount", - "value": "", - "type": "string" - }, - { - "key": "marketplaceQuery", - "value": "oud", - "type": "string" - }, - { - "key": "marketplaceMinPrice", - "value": "1000", - "type": "string" - }, - { - "key": "marketplaceMaxPrice", - "value": "5000", - "type": "string" - }, - { - "key": "marketplaceIsActive", - "value": "true", - "type": "string" - }, - { - "key": "marketplaceSortBy", - "value": "price", - "type": "string" - }, - { - "key": "repairShopQuery", - "value": "riyadh", - "type": "string" - }, - { - "key": "repairShopIsActive", - "value": "true", - "type": "string" - }, - { - "key": "repairShopSortBy", - "value": "name", - "type": "string" - }, - { - "key": "instrumentId", - "value": "", - "type": "string" - }, - { - "key": "repairShopId", - "value": "", - "type": "string" - }, - { - "key": "generatedMusicUrl", - "value": "", - "type": "string" - }, - { - "key": "aiMusicPrompt", - "value": "Calm oud melody with light percussion and emotional Arabic mood", - "type": "string" - }, - { - "key": "aiMusicDuration", - "value": "12", - "type": "string" - }, - { - "key": "aiMusicSeed", - "value": "42", - "type": "string" - }, - { - "key": "listingId", - "value": "", - "type": "string" - }, - { - "key": "marketplaceListingCategory", - "value": "accessory", - "type": "string" - }, - { - "key": "marketplaceCondition", - "value": "used", - "type": "string" - }, - { - "key": "marketplaceInstrumentType", - "value": "oud", - "type": "string" - }, - { - "key": "talentRole", - "value": "instrumentalist", - "type": "string" - }, - { - "key": "talentExperienceLevel", - "value": "", - "type": "string" - }, - { - "key": "usersHasAvatar", - "value": "true", - "type": "string" - }, - { - "key": "reactionType", - "value": "love", - "type": "string" - }, - { - "key": "followRequestId", - "value": "", - "type": "string" - }, - { - "key": "reportId", - "value": "", - "type": "string" - }, - { - "key": "repostId", - "value": "", - "type": "string" - }, - { - "key": "quotePostId", - "value": "", - "type": "string" - }, - { - "key": "replyToMessageId", - "value": "" - }, - { - "key": "mediaFilePath", - "value": "" - }, - { - "key": "collaboratorId", - "value": "" - }, - { - "key": "commentKeyword", - "value": "spam" - }, - { - "key": "reportReason", - "value": "spam" - }, - { - "key": "sessionId", - "value": "" - }, - { - "key": "caseId", - "value": "" - }, - { - "key": "historyId", - "value": "" - }, - { - "key": "bulkActionTargetId", - "value": "" - }, - { - "key": "auditAction", - "value": "" - } - ] + "info": { + "name": "Oudelaa Mobile API", + "description": "Mobile/API app collection generated from Oudelaa master collection.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Auth", + "item": [ + { + "name": "Register Basic", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/register-basic", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"password\": \"StrongPass123!\",\n \"confirmPassword\": \"StrongPass123!\"\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const ts = Date.now();", + "if (!pm.environment.get(\u0027registerEmail\u0027)) { pm.environment.set(\u0027registerEmail\u0027, `test_${ts}@example.com`); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.exist;", + "pm.expect(json.email).to.exist;", + "if (json.debugCode) { pm.environment.set(\u0027emailVerificationCode\u0027, json.debugCode); }" + ] + } + } + ] + }, + { + "name": "Register", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/register", + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Test User\",\n \"stageName\": \"Artist One\",\n \"username\": \"{{registerUsername}}\",\n \"email\": \"{{registerEmail}}\",\n \"password\": \"StrongPass123!\",\n \"confirmPassword\": \"StrongPass123!\",\n \"musicRoles\": [\"singer\", \"composer\"],\n \"musicGenres\": [\"Tarab\", \"Pop\"],\n \"favoriteInstruments\": [\"Oud\", \"Piano\"],\n \"favoriteMaqamat\": [\"Bayati\", \"Rast\"],\n \"location\": \"Riyadh, Saudi Arabia\",\n \"latitude\": 24.7136,\n \"longitude\": 46.6753,\n \"isPrivate\": false\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const ts = Date.now();", + "if (!pm.environment.get(\u0027registerEmail\u0027)) { pm.environment.set(\u0027registerEmail\u0027, `test_${ts}@example.com`); }", + "if (!pm.environment.get(\u0027registerUsername\u0027)) { pm.environment.set(\u0027registerUsername\u0027, `test_user_${ts}`); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.exist;", + "pm.expect(json.email).to.exist;", + "if (json.debugCode) { pm.environment.set(\u0027emailVerificationCode\u0027, json.debugCode); }" + ] + } + } + ] + }, + { + "name": "Login", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/login", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"password\": \"StrongPass123!\"\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const missing = [];", + "if (!pm.environment.get(\u0027accessToken\u0027)) missing.push(\u0027accessToken\u0027);", + "if (!pm.environment.get(\u0027postId\u0027)) missing.push(\u0027postId\u0027);", + "if (missing.length) { throw new Error(\u0027Missing environment vars: \u0027 + missing.join(\u0027, \u0027)); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.user).to.exist;", + "const uid = json.user._id || json.user.id;", + "pm.environment.set(\u0027accessToken\u0027, json.accessToken);", + "pm.environment.set(\u0027refreshToken\u0027, json.refreshToken);", + "pm.environment.set(\u0027userId\u0027, uid);", + "pm.environment.set(\u0027currentUserId\u0027, uid);" + ] + } + } + ] + }, + { + "name": "Target User Login", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/login", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{targetLoginEmail}}\",\n \"password\": \"{{targetLoginPassword}}\"\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const missing = [];", + "if (!pm.environment.get(\u0027accessToken\u0027)) missing.push(\u0027accessToken\u0027);", + "if (!pm.environment.get(\u0027postId\u0027)) missing.push(\u0027postId\u0027);", + "if (missing.length) { throw new Error(\u0027Missing environment vars: \u0027 + missing.join(\u0027, \u0027)); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.user).to.exist;", + "const uid = json.user._id || json.user.id;", + "pm.environment.set(\u0027targetAccessToken\u0027, json.accessToken);", + "pm.environment.set(\u0027targetRefreshToken\u0027, json.refreshToken);", + "pm.environment.set(\u0027targetUserId\u0027, uid);" + ] + } + } + ] + }, + { + "name": "Admin Login", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/login", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{adminEmail}}\",\n \"password\": \"{{adminPassword}}\"\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const missing = [];", + "if (!pm.environment.get(\u0027accessToken\u0027)) missing.push(\u0027accessToken\u0027);", + "if (!pm.environment.get(\u0027postId\u0027)) missing.push(\u0027postId\u0027);", + "if (missing.length) { throw new Error(\u0027Missing environment vars: \u0027 + missing.join(\u0027, \u0027)); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.environment.set(\u0027adminAccessToken\u0027, json.accessToken);", + "pm.environment.set(\u0027adminRefreshToken\u0027, json.refreshToken);", + "pm.environment.set(\u0027adminUserId\u0027, (json.user \u0026\u0026 (json.user._id || json.user.id)) || pm.environment.get(\u0027adminUserId\u0027));" + ] + } + } + ] + }, + { + "name": "Refresh", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/refresh", + "body": { + "mode": "raw", + "raw": "{\n \"refreshToken\": \"{{refreshToken}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.user).to.exist;", + "pm.environment.set(\u0027accessToken\u0027, json.accessToken);", + "pm.environment.set(\u0027refreshToken\u0027, json.refreshToken);", + "pm.environment.set(\u0027userId\u0027, json.user._id || json.user.id);" + ] + } + } + ] + }, + { + "name": "Logout", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/logout", + "body": { + "mode": "raw", + "raw": "{\n \"refreshToken\": \"{{refreshToken}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.eql(\u0027Logged out successfully\u0027);" + ] + } + } + ] + }, + { + "name": "Google OAuth Start (Browser)", + "request": { + "method": "GET", + "header": [ + + ], + "url": "{{baseUrl}}/auth/google", + "description": "Open this request in browser flow to start Google OAuth redirect." + } + }, + { + "name": "Google OAuth Callback (Browser Redirect)", + "request": { + "method": "GET", + "header": [ + + ], + "url": "{{baseUrl}}/auth/google/callback", + "description": "Callback endpoint is called by Google redirect after browser login. Do not call directly from Postman." + } + }, + { + "name": "Google Token Login (Mobile - Recommended)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/google/token", + "description": "Mobile-style Google login (Instagram-like): send Google idToken from app SDK.", + "body": { + "mode": "raw", + "raw": "{\n \"idToken\": \"{{googleIdToken}}\"\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const idToken = (pm.environment.get(\u0027googleIdToken\u0027) || \u0027\u0027).trim();", + "if (!idToken) {", + " throw new Error(\u0027googleIdToken is empty. Paste Google idToken from Flutter/Google SDK into environment variable googleIdToken.\u0027);", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.accessToken).to.exist;", + "pm.expect(json.refreshToken).to.exist;", + "pm.expect(json.user).to.exist;", + "const uid = json.user._id || json.user.id;", + "pm.environment.set(\u0027accessToken\u0027, json.accessToken);", + "pm.environment.set(\u0027refreshToken\u0027, json.refreshToken);", + "pm.environment.set(\u0027userId\u0027, uid);", + "pm.environment.set(\u0027currentUserId\u0027, uid);" + ] + } + } + ] + }, + { + "name": "List My Sessions", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/auth/sessions" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;", + "if (json.items \u0026\u0026 json.items.length \u003e 0) { pm.environment.set(\u0027sessionJti\u0027, json.items[0].jti); }" + ] + } + } + ] + }, + { + "name": "Revoke My Session", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/auth/sessions/{{sessionJti}}/revoke" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.success).to.eql(true);" + ] + } + } + ] + }, + { + "name": "Forgot Password", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/forgot-password", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{registerEmail}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.exist;", + "if (json.debugCode) { pm.environment.set(\u0027resetCode\u0027, json.debugCode); }" + ] + } + } + ] + }, + { + "name": "Verify Reset Code", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/verify-reset-code", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"code\": \"{{resetCode}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.resetToken).to.exist;", + "pm.environment.set(\u0027resetToken\u0027, json.resetToken);" + ] + } + } + ] + }, + { + "name": "Reset Password", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/reset-password", + "body": { + "mode": "raw", + "raw": "{\n \"resetToken\": \"{{resetToken}}\",\n \"newPassword\": \"{{newPassword}}\",\n \"confirmPassword\": \"{{newPassword}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.eql(\u0027Password reset successfully\u0027);" + ] + } + } + ] + }, + { + "name": "Send Email Verification", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/send-email-verification", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{registerEmail}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.exist;", + "if (json.debugCode) { pm.environment.set(\u0027emailVerificationCode\u0027, json.debugCode); }" + ] + } + } + ] + }, + { + "name": "Verify Email", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/verify-email", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"code\": \"{{emailVerificationCode}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.exist;", + "if (json.accessToken) { pm.environment.set(\u0027accessToken\u0027, json.accessToken); }", + "if (json.refreshToken) { pm.environment.set(\u0027refreshToken\u0027, json.refreshToken); }", + "if (json.user) { const uid = json.user._id || json.user.id; pm.environment.set(\u0027userId\u0027, uid); pm.environment.set(\u0027currentUserId\u0027, uid); }" + ] + } + } + ] + } + ] + }, + { + "name": "Media", + "item": [ + { + "name": "Media Health", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/media/health" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.storage).to.be.an(\u0027object\u0027);", + "pm.expect(json.storage.provider).to.be.oneOf([\u0027local\u0027, \u0027s3\u0027]);", + "pm.expect(json.serving.rangeRequests).to.eql(true);", + "pm.expect(json.processing).to.be.an(\u0027object\u0027);" + ] + } + } + ] + } + ] + }, + { + "name": "Users", + "item": [ + { + "name": "Search Users", + "request": { + "method": "GET", + "url": "{{baseUrl}}/users?q={{usersQuery}}\u0026isVerified={{usersVerified}}\u0026musicRole={{talentRole}}\u0026experienceLevel={{talentExperienceLevel}}\u0026hasAvatar={{usersHasAvatar}}\u0026sortBy={{usersSortBy}}\u0026sortOrder={{listSortOrder}}\u0026page=1\u0026limit=20", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.pagination).to.exist;", + "const currentUserId = pm.environment.get(\u0027currentUserId\u0027);", + "if (json.items \u0026\u0026 json.items.length \u003e 0) {", + " const target = json.items.find(u =\u003e (u._id || u.id) !== currentUserId) || json.items[0];", + " const targetId = target._id || target.id;", + " pm.environment.set(\u0027targetUserId\u0027, targetId);", + " if (target.username) { pm.environment.set(\u0027targetUsername\u0027, target.username); }", + "}" + ] + } + } + ] + }, + { + "name": "Profile Setup", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/users/me/profile-setup", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "stageName", + "value": "Artist One", + "type": "text" + }, + { + "key": "avatarFile", + "type": "file", + "src": [ + + ] + }, + { + "key": "bio", + "value": "Short bio", + "type": "text" + }, + { + "key": "location", + "value": "Riyadh, Saudi Arabia", + "type": "text" + }, + { + "key": "latitude", + "value": "24.7136", + "type": "text" + }, + { + "key": "coverImageFile", + "type": "file", + "src": [ + + ], + "disabled": true + }, + { + "key": "longitude", + "value": "46.6753", + "type": "text" + } + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.location).to.eql(\u0027Riyadh, Saudi Arabia\u0027);", + "pm.expect(json.latitude).to.eql(24.7136);", + "pm.expect(json.longitude).to.eql(46.6753);", + "if (json.avatar) { pm.expect(json.avatar).to.be.a(\u0027string\u0027); }", + "if (json.coverImage) { pm.expect(json.coverImage).to.be.a(\u0027string\u0027); }" + ] + } + } + ] + }, + { + "name": "Music Setup", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/users/me/music-setup", + "body": { + "mode": "raw", + "raw": "{\n \"musicRoles\": [\"instrumentalist\", \"composer\"],\n \"musicGenres\": [\"Tarab\", \"Pop\"],\n \"experienceLevel\": \"intermediate\",\n \"favoriteInstruments\": [\"Oud\", \"Piano\"],\n \"favoriteMaqamat\": [\"Bayati\", \"Rast\"]\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.musicRoles).to.include(\u0027instrumentalist\u0027);", + "pm.expect(json.musicRoles).to.include(\u0027composer\u0027);", + "pm.expect(json.experienceLevel).to.eql(\u0027intermediate\u0027);", + "pm.expect(json).to.not.have.property(\u0027isInstrumentalist\u0027);", + "pm.expect(json).to.not.have.property(\u0027isSinger\u0027);", + "pm.expect(json).to.not.have.property(\u0027isComposer\u0027);", + "pm.expect(json).to.not.have.property(\u0027isLyricist\u0027);" + ] + } + } + ] + }, + { + "name": "Update Me", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/users/me", + "body": { + "mode": "raw", + "raw": "{\n \"bio\": \"Updated from Postman\",\n \"avatar\": \"https://cdn.example.com/avatar.jpg\",\n \"stageName\": \"Artist One Updated\",\n \"musicGenres\": [\"Tarab\", \"Khaleeji\"],\n \"favoriteInstruments\": [\"Oud\"],\n \"favoriteMaqamat\": [\"Hijaz\"],\n \"location\": \"Jeddah, Saudi Arabia\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.bio).to.eql(\u0027Updated from Postman\u0027);", + "if (json.coverImage) { pm.expect(json.coverImage).to.be.a(\u0027string\u0027); }" + ] + } + } + ] + }, + { + "name": "Update Me With Files", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/users/me", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "stageName", + "value": "Artist One Files", + "type": "text" + }, + { + "key": "bio", + "value": "Updated from Postman with files", + "type": "text" + }, + { + "key": "location", + "value": "Riyadh, Saudi Arabia", + "type": "text" + }, + { + "key": "avatar", + "value": "https://cdn.example.com/avatar.jpg", + "type": "text" + }, + { + "key": "avatarFile", + "type": "file", + "src": [ + + ] + }, + { + "key": "coverImageFile", + "type": "file", + "src": [ + + ], + "disabled": true + } + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.stageName).to.eql(\u0027Artist One Files\u0027);", + "if (json.avatar) { pm.expect(json.avatar).to.be.a(\u0027string\u0027); }", + "if (json.coverImage) { pm.expect(json.coverImage).to.be.a(\u0027string\u0027); }" + ] + } + } + ] + }, + { + "name": "Get User By Id", + "request": { + "method": "GET", + "url": "{{baseUrl}}/users/{{userId}}", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ] + } + }, + { + "name": "Discover Talents", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/users/discover?q={{usersQuery}}\u0026musicRole={{talentRole}}\u0026experienceLevel={{talentExperienceLevel}}\u0026hasAvatarOnly={{usersHasAvatar}}\u0026includeRoleBuckets=true\u0026sortBy=followersCount\u0026sortOrder={{listSortOrder}}\u0026page=1\u0026limit=8" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json).to.have.property(\u0027roleBuckets\u0027);", + "pm.expect(json.pagination).to.exist;", + "if (json.items \u0026\u0026 json.items.length \u003e 0) { pm.environment.set(\u0027targetUserId\u0027, json.items[0]._id || json.items[0].id); }" + ] + } + } + ] + }, + { + "name": "Get Profile Overview", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/users/{{targetUserId}}/profile-overview" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property(\u0027stats\u0027);", + "pm.expect(json).to.have.property(\u0027contentCounts\u0027);", + "pm.expect(json).to.have.property(\u0027viewerState\u0027);" + ] + } + } + ] + }, + { + "name": "Get User Presence", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/{{userId}}/presence", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Presence shape\u0027, function () { const json = pm.response.json(); pm.expect(json).to.have.property(\u0027userId\u0027); pm.expect(json).to.have.property(\u0027isOnline\u0027); pm.expect(json).to.have.property(\u0027lastSeenAt\u0027); });" + ] + } + } + }, + { + "name": "Get User Posts By User Id", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/{{userId}}/posts?page=1\u0026limit=20\u0026sortOrder={{listSortOrder}}", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Paginated user posts\u0027, function () { const json = pm.response.json(); pm.expect(json).to.have.property(\u0027items\u0027); pm.expect(json).to.have.property(\u0027pagination\u0027); });" + ] + } + } + } + ] + }, + { + "name": "Posts", + "item": [ + { + "name": "Create Post (Own)", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "content", + "value": "First text post with mention @{{targetUsername}} #music", + "type": "text" + }, + { + "key": "taggedUserIds", + "value": "{{targetUserId}}", + "type": "text" + }, + { + "key": "mentionUsernames", + "value": "{{targetUsername}}", + "type": "text" + }, + { + "key": "location", + "value": "Riyadh, Saudi Arabia", + "type": "text" + }, + { + "key": "latitude", + "value": "24.7136", + "type": "text" + }, + { + "key": "longitude", + "value": "46.6753", + "type": "text" + }, + { + "key": "visibility", + "value": "public", + "type": "text" + } + ] + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (!pm.environment.get(\u0027accessToken\u0027)) { throw new Error(\u0027Missing environment var: accessToken\u0027); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "const pid = json._id || json.id;", + "pm.environment.set(\u0027postId\u0027, pid);", + "pm.environment.set(\u0027ownPostId\u0027, pid);" + ] + } + } + ] + }, + { + "name": "Create Carousel Post (Instagram Style)", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "content", + "value": "Weekend jam session #oud #music", + "type": "text" + }, + { + "key": "visibility", + "value": "public", + "type": "text" + }, + { + "key": "taggedUserIds", + "value": "{{targetUserId}}", + "type": "text" + }, + { + "key": "mentionUsernames", + "value": "{{targetUsername}}", + "type": "text" + }, + { + "key": "location", + "value": "Riyadh, Saudi Arabia", + "type": "text" + }, + { + "key": "latitude", + "value": "24.7136", + "type": "text" + }, + { + "key": "longitude", + "value": "46.6753", + "type": "text" + }, + { + "key": "imageFiles", + "type": "file", + "src": [ + + ] + }, + { + "key": "imageFiles", + "type": "file", + "src": [ + + ] + } + ] + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (!pm.environment.get(\u0027accessToken\u0027)) { throw new Error(\u0027Missing environment var: accessToken\u0027); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.postType).to.eql(\u0027image\u0027);", + "pm.expect(json.processingStatus).to.eql(\u0027ready\u0027);", + "pm.expect(json.media).to.be.an(\u0027object\u0027);", + "pm.expect(json.media.mediaType).to.eql(json.postType);", + "pm.expect(json.imageUrls).to.be.an(\u0027array\u0027);", + "pm.expect(json.media.images).to.be.an(\u0027array\u0027).that.is.not.empty;", + "pm.expect(json.media.images[0].url).to.be.a(\u0027string\u0027);", + "pm.expect(json.imageUrls.length).to.be.greaterThan(0);", + "const pid = json._id || json.id;", + "pm.environment.set(\u0027postId\u0027, pid);", + "pm.environment.set(\u0027ownPostId\u0027, pid);" + ] + } + } + ] + }, + { + "name": "Update Carousel Post (Instagram Style)", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts/{{postId}}", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "content", + "value": "Updated carousel caption #oud #live", + "type": "text" + }, + { + "key": "imageFiles", + "type": "file", + "src": [ + + ] + } + ] + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const missing = [];", + "if (!pm.environment.get(\u0027accessToken\u0027)) missing.push(\u0027accessToken\u0027);", + "if (!pm.environment.get(\u0027postId\u0027)) missing.push(\u0027postId\u0027);", + "if (missing.length) { throw new Error(\u0027Missing environment vars: \u0027 + missing.join(\u0027, \u0027)); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.postType).to.eql(\u0027image\u0027);", + "pm.expect(json.imageUrls).to.be.an(\u0027array\u0027);" + ] + } + } + ] + }, + { + "name": "Get User Posts (Own)", + "request": { + "method": "GET", + "url": "{{baseUrl}}/posts/user/{{userId}}?visibility={{postVisibility}}\u0026sortBy={{postSortBy}}\u0026sortOrder={{listSortOrder}}\u0026page=1\u0026limit=10", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const missing = [];", + "if (!pm.environment.get(\u0027accessToken\u0027)) missing.push(\u0027accessToken\u0027);", + "if (!pm.environment.get(\u0027userId\u0027)) missing.push(\u0027userId\u0027);", + "if (missing.length) { throw new Error(\u0027Missing environment vars: \u0027 + missing.join(\u0027, \u0027)); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;", + "if (!pm.environment.get(\u0027postId\u0027) \u0026\u0026 json.items \u0026\u0026 json.items.length \u003e 0) {", + " pm.environment.set(\u0027postId\u0027, json.items[0]._id || json.items[0].id);", + " pm.environment.set(\u0027ownPostId\u0027, json.items[0]._id || json.items[0].id);", + "}" + ] + } + } + ] + }, + { + "name": "Get User Posts Filtered", + "request": { + "method": "GET", + "url": "{{baseUrl}}/posts/user/{{userId}}?visibility={{postVisibility}}\u0026postType={{postTypeFilter}}\u0026q={{postSearchQuery}}\u0026hashtag={{postHashtag}}\u0026sortBy={{postSortBy}}\u0026sortOrder={{listSortOrder}}\u0026page=1\u0026limit=10", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;" + ] + } + } + ] + }, + { + "name": "Get Post By Id", + "request": { + "method": "GET", + "url": "{{baseUrl}}/posts/{{postId}}", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json._id || json.id).to.exist;", + "pm.expect(json).to.have.property(\u0027content\u0027);" + ] + } + } + ] + }, + { + "name": "Register Post View", + "request": { + "method": "POST", + "url": "{{baseUrl}}/posts/{{postId}}/view", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.success).to.eql(true);", + "pm.expect(json).to.have.property(\u0027viewCount\u0027);", + "pm.environment.set(\u0027lastViewCount\u0027, json.viewCount);" + ] + } + } + ] + }, + { + "name": "Register Post Share", + "request": { + "method": "POST", + "url": "{{baseUrl}}/posts/{{postId}}/share", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.success).to.eql(true);", + "pm.expect(json).to.have.property(\u0027shareCount\u0027);", + "pm.environment.set(\u0027lastShareCount\u0027, json.shareCount);" + ] + } + } + ] + }, + { + "name": "Update Post", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts/{{postId}}", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "content", + "value": "Updated post content with @{{targetUsername}} #live", + "type": "text" + }, + { + "key": "taggedUserIds", + "value": "{{targetUserId}}", + "type": "text" + }, + { + "key": "mentionUsernames", + "value": "{{targetUsername}}", + "type": "text" + }, + { + "key": "location", + "value": "Jeddah, Saudi Arabia", + "type": "text" + }, + { + "key": "latitude", + "value": "21.5433", + "type": "text" + }, + { + "key": "longitude", + "value": "39.1728", + "type": "text" + } + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.content).to.eql(\u0027Updated post content with @artist_one #live\u0027);" + ] + } + } + ] + }, + { + "name": "Create Video Post (Own)", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "content", + "value": "Video post #music", + "type": "text" + }, + { + "key": "visibility", + "value": "public", + "type": "text" + }, + { + "key": "durationSeconds", + "value": "42", + "type": "text" + }, + { + "key": "coverImageFile", + "type": "file", + "src": [ + + ] + }, + { + "key": "style", + "value": "Sharqi", + "type": "text" + }, + { + "key": "maqam", + "value": "Hijaz", + "type": "text" + }, + { + "key": "mentionUsernames", + "value": "{{targetUsername}}", + "type": "text" + }, + { + "key": "rhythmSignature", + "value": "6/8", + "type": "text" + }, + { + "key": "location", + "value": "Riyadh, Saudi Arabia", + "type": "text" + }, + { + "key": "latitude", + "value": "24.7136", + "type": "text" + }, + { + "key": "longitude", + "value": "46.6753", + "type": "text" + }, + { + "key": "videoFile", + "type": "file", + "src": [ + + ] + } + ] + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (!pm.environment.get(\u0027accessToken\u0027)) { throw new Error(\u0027Missing environment var: accessToken\u0027); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.postType).to.eql(\u0027video\u0027);", + "pm.expect(json.media.thumbnailUrl).to.be.a(\u0027string\u0027);", + "pm.expect(json.media.preferredPlaybackUrl).to.be.a(\u0027string\u0027);", + "if (json.hlsUrl) { pm.expect(json.media.preferredPlaybackUrl).to.eql(json.hlsUrl); }", + "pm.expect(json.processingStatus).to.eql(\u0027ready\u0027);", + "pm.expect(json.media).to.be.an(\u0027object\u0027);", + "pm.expect(json.media.mediaType).to.eql(json.postType);", + "pm.expect(json.durationSeconds).to.eql(42);", + "if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a(\u0027string\u0027); }", + "pm.expect(json.style).to.eql(\u0027Sharqi\u0027);", + "pm.expect(json.maqam).to.eql(\u0027Hijaz\u0027);", + "pm.expect(json.rhythmSignature).to.eql(\u00276/8\u0027);", + "pm.environment.set(\u0027postId\u0027, json._id || json.id);", + "pm.environment.set(\u0027ownPostId\u0027, json._id || json.id);" + ] + } + } + ] + }, + { + "name": "Create Reel (Video First)", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts/reels", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "content", + "value": "New reel from oud session #reel", + "type": "text" + }, + { + "key": "visibility", + "value": "public", + "type": "text" + }, + { + "key": "durationSeconds", + "value": "30", + "type": "text" + }, + { + "key": "coverImageFile", + "type": "file", + "src": [ + + ] + }, + { + "key": "style", + "value": "Sharqi", + "type": "text" + }, + { + "key": "maqam", + "value": "Hijaz", + "type": "text" + }, + { + "key": "rhythmSignature", + "value": "6/8", + "type": "text" + }, + { + "key": "videoFile", + "type": "file", + "src": [ + + ] + } + ] + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (!pm.environment.get(\u0027accessToken\u0027)) { throw new Error(\u0027Missing environment var: accessToken\u0027); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.postType).to.eql(\u0027video\u0027);", + "pm.expect(json.media.thumbnailUrl).to.be.a(\u0027string\u0027);", + "pm.expect(json.media.preferredPlaybackUrl).to.be.a(\u0027string\u0027);", + "if (json.hlsUrl) { pm.expect(json.media.preferredPlaybackUrl).to.eql(json.hlsUrl); }", + "pm.expect(json.processingStatus).to.eql(\u0027ready\u0027);", + "pm.expect(json.media).to.be.an(\u0027object\u0027);", + "pm.expect(json.media.mediaType).to.eql(json.postType);", + "pm.expect(json.durationSeconds).to.eql(30);", + "if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a(\u0027string\u0027); }", + "pm.expect(json.style).to.eql(\u0027Sharqi\u0027);", + "pm.expect(json.maqam).to.eql(\u0027Hijaz\u0027);", + "pm.expect(json.rhythmSignature).to.eql(\u00276/8\u0027);", + "pm.environment.set(\u0027postId\u0027, json._id || json.id);", + "pm.environment.set(\u0027ownPostId\u0027, json._id || json.id);" + ] + } + } + ] + }, + { + "name": "Get Reels", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts/reels?visibility={{postVisibility}}\u0026authorId={{userId}}\u0026sortBy={{postSortBy}}\u0026sortOrder={{listSortOrder}}\u0026page=1\u0026limit=10" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;" + ] + } + } + ] + }, + { + "name": "Get Reels Filtered", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts/reels?visibility={{postVisibility}}\u0026authorId={{userId}}\u0026q={{reelQuery}}\u0026sortBy={{postSortBy}}\u0026sortOrder={{listSortOrder}}\u0026page=1\u0026limit=10" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;" + ] + } + } + ] + }, + { + "name": "Create Audio Post (Own)", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "content", + "value": "Audio post #oud", + "type": "text" + }, + { + "key": "visibility", + "value": "public", + "type": "text" + }, + { + "key": "durationSeconds", + "value": "54", + "type": "text" + }, + { + "key": "coverImageFile", + "type": "file", + "src": [ + + ] + }, + { + "key": "style", + "value": "Sharqi", + "type": "text" + }, + { + "key": "maqam", + "value": "Hijaz", + "type": "text" + }, + { + "key": "rhythmSignature", + "value": "6/8", + "type": "text" + }, + { + "key": "audioFile", + "type": "file", + "src": [ + + ] + } + ] + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (!pm.environment.get(\u0027accessToken\u0027)) { throw new Error(\u0027Missing environment var: accessToken\u0027); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.postType).to.eql(\u0027audio\u0027);", + "pm.expect(json.media.audioUrl).to.be.a(\u0027string\u0027);", + "pm.expect(json.media.waveformPeaks).to.be.an(\u0027array\u0027);", + "pm.expect(json.media.durationSeconds).to.exist;", + "pm.expect(json.processingStatus).to.eql(\u0027ready\u0027);", + "pm.expect(json.media).to.be.an(\u0027object\u0027);", + "pm.expect(json.media.mediaType).to.eql(json.postType);", + "pm.expect(json.durationSeconds).to.eql(54);", + "if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a(\u0027string\u0027); }", + "pm.expect(json.style).to.eql(\u0027Sharqi\u0027);", + "pm.expect(json.maqam).to.eql(\u0027Hijaz\u0027);", + "pm.expect(json.rhythmSignature).to.eql(\u00276/8\u0027);", + "pm.environment.set(\u0027postId\u0027, json._id || json.id);", + "pm.environment.set(\u0027ownPostId\u0027, json._id || json.id);" + ] + } + } + ] + }, + { + "name": "Create Audio Post (URL + Waveform)", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "content", + "value": "Audio post with waveform #oud #hijaz", + "type": "text" + }, + { + "key": "audioUrl", + "value": "https://cdn.example.com/audio-sample.mp3", + "type": "text" + }, + { + "key": "durationSeconds", + "value": "54", + "type": "text" + }, + { + "key": "thumbnailUrl", + "value": "https://cdn.example.com/audio-cover.jpg", + "type": "text" + }, + { + "key": "style", + "value": "Sharqi", + "type": "text" + }, + { + "key": "maqam", + "value": "Hijaz", + "type": "text" + }, + { + "key": "rhythmSignature", + "value": "6/8", + "type": "text" + }, + { + "key": "waveformPeaks", + "value": "[12,38,27,49,22,44]", + "type": "text" + }, + { + "key": "visibility", + "value": "public", + "type": "text" + } + ] + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (!pm.environment.get(\u0027accessToken\u0027)) { throw new Error(\u0027Missing environment var: accessToken\u0027); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.postType).to.eql(\u0027audio\u0027);", + "pm.expect(json.media.audioUrl).to.be.a(\u0027string\u0027);", + "pm.expect(json.media.waveformPeaks).to.be.an(\u0027array\u0027);", + "pm.expect(json.media.durationSeconds).to.exist;", + "pm.expect(json.processingStatus).to.eql(\u0027ready\u0027);", + "pm.expect(json.media).to.be.an(\u0027object\u0027);", + "pm.expect(json.media.mediaType).to.eql(json.postType);", + "pm.expect(json.waveformPeaks).to.be.an(\u0027array\u0027);", + "pm.expect(json.waveformPeaks.length).to.eql(6);", + "pm.expect(json.maqam).to.eql(\u0027Hijaz\u0027);", + "pm.environment.set(\u0027postId\u0027, json._id || json.id);", + "pm.environment.set(\u0027ownPostId\u0027, json._id || json.id);" + ] + } + } + ] + }, + { + "name": "Register Post Play (Audio/Video)", + "request": { + "method": "POST", + "url": "{{baseUrl}}/posts/{{postId}}/play", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.success).to.eql(true);", + "pm.expect(json).to.have.property(\u0027playCount\u0027);", + "pm.environment.set(\u0027lastPlayCount\u0027, json.playCount);" + ] + } + } + ] + }, + { + "name": "Create Invalid Post (Video+Audio)", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "content", + "value": "Invalid post", + "type": "text" + }, + { + "key": "videoUrl", + "value": "https://cdn.example.com/video.mp4", + "type": "text" + }, + { + "key": "audioUrl", + "value": "https://cdn.example.com/audio.mp3", + "type": "text" + }, + { + "key": "visibility", + "value": "public", + "type": "text" + } + ] + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (!pm.environment.get(\u0027accessToken\u0027)) { throw new Error(\u0027Missing environment var: accessToken\u0027); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 400\u0027, function () { pm.response.to.have.status(400); });" + ] + } + } + ] + }, + { + "name": "Update Post Video (Form-Data)", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts/{{postId}}", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "content", + "value": "Updated with video file", + "type": "text" + }, + { + "key": "durationSeconds", + "value": "58", + "type": "text" + }, + { + "key": "coverImageFile", + "type": "file", + "src": [ + + ] + }, + { + "key": "style", + "value": "Contemporary", + "type": "text" + }, + { + "key": "maqam", + "value": "Bayati", + "type": "text" + }, + { + "key": "rhythmSignature", + "value": "4/4", + "type": "text" + }, + { + "key": "videoFile", + "type": "file", + "src": [ + + ] + } + ] + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const missing = [];", + "if (!pm.environment.get(\u0027accessToken\u0027)) missing.push(\u0027accessToken\u0027);", + "if (!pm.environment.get(\u0027postId\u0027)) missing.push(\u0027postId\u0027);", + "if (missing.length) { throw new Error(\u0027Missing environment vars: \u0027 + missing.join(\u0027, \u0027)); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.postType).to.eql(\u0027video\u0027);", + "pm.expect(json.durationSeconds).to.eql(58);", + "if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a(\u0027string\u0027); }", + "pm.expect(json.style).to.eql(\u0027Contemporary\u0027);", + "pm.expect(json.maqam).to.eql(\u0027Bayati\u0027);", + "pm.expect(json.rhythmSignature).to.eql(\u00274/4\u0027);" + ] + } + } + ] + }, + { + "name": "Update Post Audio (Form-Data)", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts/{{postId}}", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "content", + "value": "Updated with audio file", + "type": "text" + }, + { + "key": "durationSeconds", + "value": "61", + "type": "text" + }, + { + "key": "coverImageFile", + "type": "file", + "src": [ + + ] + }, + { + "key": "style", + "value": "Tarab", + "type": "text" + }, + { + "key": "maqam", + "value": "Nahawand", + "type": "text" + }, + { + "key": "rhythmSignature", + "value": "6/8", + "type": "text" + }, + { + "key": "audioFile", + "type": "file", + "src": [ + + ] + } + ] + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const missing = [];", + "if (!pm.environment.get(\u0027accessToken\u0027)) missing.push(\u0027accessToken\u0027);", + "if (!pm.environment.get(\u0027postId\u0027)) missing.push(\u0027postId\u0027);", + "if (missing.length) { throw new Error(\u0027Missing environment vars: \u0027 + missing.join(\u0027, \u0027)); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.postType).to.eql(\u0027audio\u0027);", + "pm.expect(json.durationSeconds).to.eql(61);", + "if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a(\u0027string\u0027); }", + "pm.expect(json.style).to.eql(\u0027Tarab\u0027);", + "pm.expect(json.maqam).to.eql(\u0027Nahawand\u0027);", + "pm.expect(json.rhythmSignature).to.eql(\u00276/8\u0027);" + ] + } + } + ] + }, + { + "name": "Delete Post (Cleanup - Run Last)", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts/{{postId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.success).to.eql(true);" + ] + } + } + ] + }, + { + "name": "Repost Post", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/posts/{{postId}}/repost", + "body": { + "mode": "raw", + "raw": "{\n \"content\": \"\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.environment.set(\u0027repostId\u0027, json._id || json.id);" + ] + } + } + ] + }, + { + "name": "Quote Post", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/posts/{{postId}}/repost", + "body": { + "mode": "raw", + "raw": "{\n \"content\": \"My take on this post\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.environment.set(\u0027quotePostId\u0027, json._id || json.id);" + ] + } + } + ] + }, + { + "name": "Update Post Comment Settings", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/posts/{{postId}}/comment-settings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "posts", + "{{postId}}", + "comment-settings" + ], + "query": [ + + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"commentsDisabled\": false,\n \"commentsFollowersOnly\": true,\n \"commentFilterKeywords\": [\n \"{{commentKeyword}}\",\n \"offensive\"\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "Pin Post To Profile", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/posts/{{postId}}/pin-profile", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "posts", + "{{postId}}", + "pin-profile" + ], + "query": [ + + ] + } + } + }, + { + "name": "Unpin Post From Profile", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/posts/{{postId}}/unpin-profile", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "posts", + "{{postId}}", + "unpin-profile" + ], + "query": [ + + ] + } + } + }, + { + "name": "Archive Post", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/posts/{{postId}}/archive", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "posts", + "{{postId}}", + "archive" + ], + "query": [ + + ] + } + } + }, + { + "name": "Restore Archived Post", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/posts/{{postId}}/restore-archive", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "posts", + "{{postId}}", + "restore-archive" + ], + "query": [ + + ] + } + } + }, + { + "name": "Create Carousel Post With Metadata", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/posts", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "posts" + ], + "query": [ + + ] + }, + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "content", + "value": "Carousel caption #oud", + "type": "text" + }, + { + "key": "imageUrls", + "value": "https://cdn.example.com/1.jpg,https://cdn.example.com/2.jpg", + "type": "text" + }, + { + "key": "imageCaptions", + "value": "First image,Second image", + "type": "text" + }, + { + "key": "imageAltTexts", + "value": "Oud on stage,Audience closeup", + "type": "text" + }, + { + "key": "collaboratorIds", + "value": "{{collaboratorId}}", + "type": "text" + }, + { + "key": "commentsDisabled", + "value": "false", + "type": "text" + }, + { + "key": "commentsFollowersOnly", + "value": "false", + "type": "text" + } + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\"Request succeeded\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201]); });", + "const json = pm.response.json();", + "const value = json.item;", + "if (value) pm.environment.set(\"postId\", value._id || value.id || value);" + ] + } + } + ] + }, + { + "name": "Update Carousel Alt Text", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/posts/{{postId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "posts", + "{{postId}}" + ], + "query": [ + + ] + }, + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "imageUrls", + "value": "https://cdn.example.com/1.jpg,https://cdn.example.com/2.jpg", + "type": "text" + }, + { + "key": "imageCaptions", + "value": "Updated first,Updated second", + "type": "text" + }, + { + "key": "imageAltTexts", + "value": "Updated oud alt,Updated audience alt", + "type": "text" + } + ] + } + } + }, + { + "name": "Disable Comments On Post", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/posts/{{postId}}/comment-settings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "posts", + "{{postId}}", + "comment-settings" + ], + "query": [ + + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"commentsDisabled\": true,\n \"commentsFollowersOnly\": false,\n \"commentFilterKeywords\": []\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "Enable Comments On Post", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/posts/{{postId}}/comment-settings", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "posts", + "{{postId}}", + "comment-settings" + ], + "query": [ + + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"commentsDisabled\": false,\n \"commentsFollowersOnly\": false,\n \"commentFilterKeywords\": []\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + } + ] + }, + { + "name": "Comments", + "item": [ + { + "name": "Create Comment", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/comments", + "body": { + "mode": "raw", + "raw": "{\n \"postId\": \"{{postId}}\",\n \"content\": \"Awesome post!\"\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const missing = [];", + "if (!pm.environment.get(\u0027accessToken\u0027)) missing.push(\u0027accessToken\u0027);", + "if (!pm.environment.get(\u0027postId\u0027)) missing.push(\u0027postId\u0027);", + "if (missing.length) { throw new Error(\u0027Missing environment vars: \u0027 + missing.join(\u0027, \u0027)); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "const cid = json._id || json.id;", + "pm.environment.set(\u0027commentId\u0027, cid);", + "pm.environment.set(\u0027ownCommentId\u0027, cid);" + ] + } + } + ] + }, + { + "name": "Create Comment With Mention", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/comments", + "body": { + "mode": "raw", + "raw": "{\n \"postId\": \"{{postId}}\",\n \"content\": \"Awesome post @{{targetUsername}}!\",\n \"mentionUsernames\": [\"{{targetUsername}}\"]\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const missing = [];", + "if (!pm.environment.get(\u0027accessToken\u0027)) missing.push(\u0027accessToken\u0027);", + "if (!pm.environment.get(\u0027postId\u0027)) missing.push(\u0027postId\u0027);", + "if (!pm.environment.get(\u0027targetUsername\u0027)) missing.push(\u0027targetUsername\u0027);", + "if (missing.length) { throw new Error(\u0027Missing environment vars: \u0027 + missing.join(\u0027, \u0027)); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "const cid = json._id || json.id;", + "pm.environment.set(\u0027commentId\u0027, cid);", + "pm.expect(json.mentionUsernames || []).to.include(pm.environment.get(\u0027targetUsername\u0027).toLowerCase());" + ] + } + } + ] + }, + { + "name": "Get Post Comments", + "request": { + "method": "GET", + "url": "{{baseUrl}}/comments/post/{{postId}}?page=1\u0026limit=10\u0026sortBy={{commentSortBy}}\u0026sortOrder={{listSortOrder}}", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;" + ] + } + } + ] + }, + { + "name": "Create Comment Reply", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/comments/{{commentId}}/replies", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "comments", + "{{commentId}}", + "replies" + ], + "query": [ + + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"content\": \"Reply to comment\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\"Request succeeded\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201]); });", + "const json = pm.response.json();", + "const value = json.item;", + "if (value) pm.environment.set(\"replyCommentId\", value._id || value.id || value);" + ] + } + } + ] + }, + { + "name": "Get Comment Replies", + "request": { + "method": "GET", + "url": "{{baseUrl}}/comments/{{commentId}}/replies?page=1\u0026limit=10\u0026sortBy={{commentSortBy}}\u0026sortOrder={{listSortOrder}}", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;" + ] + } + } + ] + }, + { + "name": "Update Comment", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/comments/{{commentId}}", + "body": { + "mode": "raw", + "raw": "{\n \"content\": \"Edited comment @{{secondaryUsername}}\",\n \"mentionUsernames\": [\"{{secondaryUsername}}\"]\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.content).to.include(\u0027Edited comment\u0027);" + ] + } + } + ] + }, + { + "name": "Delete Comment", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/comments/{{commentId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Pin Comment", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/comments/{{commentId}}/pin", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "comments", + "{{commentId}}", + "pin" + ], + "query": [ + + ] + } + } + }, + { + "name": "Unpin Comment", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/comments/{{commentId}}/unpin", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "comments", + "{{commentId}}", + "unpin" + ], + "query": [ + + ] + } + } + }, + { + "name": "Create Comment Hidden By Filter", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/comments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "comments" + ], + "query": [ + + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"postId\": \"{{postId}}\",\n \"content\": \"This contains {{commentKeyword}} keyword\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "Get Post Comments - Top", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/comments/post/{{postId}}?page=1\u0026limit=20\u0026sortBy=top\u0026sortOrder=desc", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "comments", + "post", + "{{postId}}" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "limit", + "value": "20" + }, + { + "key": "sortBy", + "value": "top" + }, + { + "key": "sortOrder", + "value": "desc" + } + ] + } + } + }, + { + "name": "Get Post Comments - Newest", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/comments/post/{{postId}}?page=1\u0026limit=20\u0026sortBy=createdAt\u0026sortOrder=desc", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "comments", + "post", + "{{postId}}" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "limit", + "value": "20" + }, + { + "key": "sortBy", + "value": "createdAt" + }, + { + "key": "sortOrder", + "value": "desc" + } + ] + } + } + } + ] + }, + { + "name": "Likes", + "item": [ + { + "name": "Like Post (Set liked=true)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/likes", + "body": { + "mode": "raw", + "raw": "{\n \"targetId\": \"{{postId}}\",\n \"targetType\": \"post\"\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const missing = [];", + "if (!pm.environment.get(\u0027accessToken\u0027)) missing.push(\u0027accessToken\u0027);", + "if (!pm.environment.get(\u0027postId\u0027)) missing.push(\u0027postId\u0027);", + "if (missing.length) { throw new Error(\u0027Missing environment vars: \u0027 + missing.join(\u0027, \u0027)); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.liked).to.eql(true);" + ] + } + } + ] + }, + { + "name": "Unlike Post (Set liked=false)", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/likes/post/{{postId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.liked).to.eql(false);" + ] + } + } + ] + }, + { + "name": "Get Post Like Status", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/likes/status/post/{{postId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property(\u0027liked\u0027);" + ] + } + } + ] + }, + { + "name": "Like Comment", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/likes", + "body": { + "mode": "raw", + "raw": "{\n \"targetId\": \"{{commentId}}\",\n \"targetType\": \"comment\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.liked).to.eql(true);" + ] + } + } + ] + }, + { + "name": "Unlike Comment", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/likes/comment/{{commentId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.liked).to.eql(false);" + ] + } + } + ] + }, + { + "name": "Get Comment Like Status", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/likes/status/comment/{{commentId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property(\u0027liked\u0027);" + ] + } + } + ] + }, + { + "name": "React To Post", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/likes", + "body": { + "mode": "raw", + "raw": "{\n \"targetId\": \"{{postId}}\",\n \"targetType\": \"post\",\n \"reactionType\": \"{{reactionType}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.reactionType).to.exist;" + ] + } + } + ] + }, + { + "name": "Reaction Status - Post", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/likes/status/post/{{postId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property(\u0027reactionSummary\u0027);" + ] + } + } + ] + }, + { + "name": "Toggle Like", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/likes/toggle", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "likes", + "toggle" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"targetType\": \"post\",\n \"targetId\": \"{{postId}}\",\n \"reactionType\": \"like\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + } + ] + }, + { + "name": "Saves", + "item": [ + { + "name": "Save Post (Set saved=true)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/saves", + "body": { + "mode": "raw", + "raw": "{\n \"postId\": \"{{postId}}\"\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const missing = [];", + "if (!pm.environment.get(\u0027accessToken\u0027)) missing.push(\u0027accessToken\u0027);", + "if (!pm.environment.get(\u0027postId\u0027)) missing.push(\u0027postId\u0027);", + "if (missing.length) { throw new Error(\u0027Missing environment vars: \u0027 + missing.join(\u0027, \u0027)); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.saved).to.eql(true);" + ] + } + } + ] + }, + { + "name": "Unsave Post (Set saved=false)", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/saves/{{postId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.saved).to.eql(false);" + ] + } + } + ] + }, + { + "name": "Get Save Status", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/saves/status/{{postId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property(\u0027saved\u0027);" + ] + } + } + ] + }, + { + "name": "Get My Saved Posts", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/saves/me?page=1\u0026limit=10\u0026sortOrder={{listSortOrder}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;" + ] + } + } + ] + }, + { + "name": "Toggle Save", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/saves/toggle", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "saves", + "toggle" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"postId\": \"{{postId}}\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + } + ] + }, + { + "name": "Follows", + "item": [ + { + "name": "Follow User (Instagram Style)", + "request": { + "method": "POST", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/{{targetUserId}}/follow", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Follow response\u0027, function () { const json = pm.response.json(); pm.expect(json).to.have.property(\u0027isFollowing\u0027); pm.expect(json).to.have.property(\u0027targetUserId\u0027); });" + ] + } + } + }, + { + "name": "Unfollow User (Instagram Style)", + "request": { + "method": "DELETE", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/{{targetUserId}}/follow", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Unfollow response\u0027, function () { const json = pm.response.json(); pm.expect(json.isFollowing).to.eql(false); });" + ] + } + } + }, + { + "name": "Get User Follow Status", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/{{targetUserId}}/follow-status", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Follow status shape\u0027, function () { const json = pm.response.json(); pm.expect(json).to.have.property(\u0027isFollowing\u0027); pm.expect(json).to.have.property(\u0027isFollowedBy\u0027); pm.expect(json).to.have.property(\u0027isMutual\u0027); });" + ] + } + } + }, + { + "name": "Get User Followers", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/{{userId}}/followers?page=1\u0026limit=20\u0026sortOrder={{listSortOrder}}", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Followers list shape\u0027, function () { const json = pm.response.json(); pm.expect(json).to.have.property(\u0027items\u0027); pm.expect(json).to.have.property(\u0027pagination\u0027); });" + ] + } + } + }, + { + "name": "Get User Following", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/{{userId}}/following?page=1\u0026limit=20\u0026sortOrder={{listSortOrder}}", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Following list shape\u0027, function () { const json = pm.response.json(); pm.expect(json).to.have.property(\u0027items\u0027); pm.expect(json).to.have.property(\u0027pagination\u0027); });" + ] + } + } + }, + { + "name": "Get My Followers", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/me/followers?page=1\u0026limit=20\u0026sortOrder={{listSortOrder}}", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + }, + { + "name": "Get My Following", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/users/me/following?page=1\u0026limit=20\u0026sortOrder={{listSortOrder}}", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + }, + { + "name": "Toggle Follow User", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/follows/toggle", + "body": { + "mode": "raw", + "raw": "{\n \"targetUserId\": \"{{targetUserId}}\"\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const missing = [];", + "if (!pm.environment.get(\u0027accessToken\u0027)) missing.push(\u0027accessToken\u0027);", + "if (!pm.environment.get(\u0027targetUserId\u0027)) missing.push(\u0027targetUserId\u0027);", + "if (missing.length) { throw new Error(\u0027Missing environment vars: \u0027 + missing.join(\u0027, \u0027)); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property(\u0027following\u0027);", + "pm.environment.set(\u0027lastFollowingState\u0027, json.following ? \u0027following\u0027 : \u0027not_following\u0027);" + ] + } + } + ] + }, + { + "name": "Get Followers", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/follows/followers/{{userId}}?page=1\u0026limit=10\u0026sortOrder={{listSortOrder}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;" + ] + } + } + ] + }, + { + "name": "Get Following", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/follows/following/{{userId}}?page=1\u0026limit=10\u0026sortOrder={{listSortOrder}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;" + ] + } + } + ] + }, + { + "name": "Get Follow Status", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/follows/status/{{targetUserId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property(\u0027following\u0027);" + ] + } + } + ] + }, + { + "name": "Get Follow Suggestions", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/follows/suggestions?page=1\u0026limit=10\u0026sortOrder={{listSortOrder}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;" + ] + } + } + ] + }, + { + "name": "List Follow Requests", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/follows/requests?page=1\u0026limit=20" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "if (json.items \u0026\u0026 json.items.length) { pm.environment.set(\u0027followRequestId\u0027, json.items[0]._id || json.items[0].id); }" + ] + } + } + ] + }, + { + "name": "Approve Follow Request", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/follows/requests/{{followRequestId}}/approve" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Reject Follow Request", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/follows/requests/{{followRequestId}}/reject" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });" + ] + } + } + ] + } + ] + }, + { + "name": "Notifications", + "item": [ + { + "name": "Get My Notifications", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/notifications?read={{notificationRead}}\u0026sortOrder={{listSortOrder}}\u0026page=1\u0026limit=20" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;", + "if (json.items \u0026\u0026 json.items.length \u003e 0) {", + " pm.environment.set(\u0027notificationId\u0027, json.items[0]._id || json.items[0].id);", + " pm.expect(json.items[0]).to.have.property(\u0027title\u0027);", + " pm.expect(json.items[0]).to.have.property(\u0027resourceType\u0027);", + " pm.expect(json.items[0]).to.have.property(\u0027deepLink\u0027);", + "}" + ] + } + } + ] + }, + { + "name": "Get My Notifications Filtered", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/notifications?read={{notificationRead}}\u0026type={{notificationType}}\u0026resourceType={{notificationResourceType}}\u0026sortOrder={{listSortOrder}}\u0026page=1\u0026limit=20" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;" + ] + } + } + ] + }, + { + "name": "Get Unread Notifications Count", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/notifications/unread-count" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property(\u0027unreadCount\u0027);" + ] + } + } + ] + }, + { + "name": "Mark Notification Read", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/notifications/{{notificationId}}/read" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property(\u0027unreadCount\u0027);" + ] + } + } + ] + }, + { + "name": "Mark All Notifications Read", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/notifications/read-all" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.unreadCount).to.eql(0);" + ] + } + } + ] + } + ] + }, + { + "name": "Chat", + "item": [ + { + "name": "Create Conversation", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/conversations", + "body": { + "mode": "raw", + "raw": "{\n \"participantIds\": [\"{{targetUserId}}\"],\n \"isGroup\": false\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const missing = [];", + "if (!pm.environment.get(\u0027accessToken\u0027)) missing.push(\u0027accessToken\u0027);", + "if (!pm.environment.get(\u0027targetUserId\u0027)) missing.push(\u0027targetUserId\u0027);", + "if (missing.length) { throw new Error(\u0027Missing environment vars: \u0027 + missing.join(\u0027, \u0027)); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.environment.set(\u0027conversationId\u0027, json._id || json.id);" + ] + } + } + ] + }, + { + "name": "Get My Conversations", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/conversations?limit=20\u0026cursor={{chatConversationsCursor}}\u0026sortOrder={{listSortOrder}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.pagination).to.exist;", + "if (json.items \u0026\u0026 json.items.length \u003e 0) { pm.environment.set(\u0027conversationId\u0027, json.items[0]._id || json.items[0].id); }", + "pm.environment.set(\u0027chatConversationsCursor\u0027, json.nextCursor || \u0027\u0027);" + ] + } + } + ] + }, + { + "name": "Send Message", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/chat/messages", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "chat", + "messages" + ], + "query": [ + + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello from Postman\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\"Request succeeded\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201]); });", + "const json = pm.response.json();", + "const value = json.item;", + "if (value) pm.environment.set(\"messageId\", value._id || value.id || value);" + ] + } + } + ] + }, + { + "name": "Get Conversation Messages", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/conversations/{{conversationId}}/messages?limit=20\u0026cursor={{chatMessagesCursor}}\u0026sortOrder={{listSortOrder}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.pagination).to.exist;", + "pm.environment.set(\u0027chatMessagesCursor\u0027, json.nextCursor || \u0027\u0027);" + ] + } + } + ] + }, + { + "name": "Mark Message Seen", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/messages/{{messageId}}/seen" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Unsend Message", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/messages/{{messageId}}/unsend" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Block User In Chat", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/blocks/{{targetUserId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.blocked).to.eql(true);" + ] + } + } + ] + }, + { + "name": "Unblock User In Chat", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/blocks/{{targetUserId}}/unblock" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Get Chat Block Status", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/blocks/status/{{targetUserId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property(\u0027iBlocked\u0027);", + "pm.expect(json).to.have.property(\u0027blockedMe\u0027);" + ] + } + } + ] + }, + { + "name": "Get My Chat Blocks", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/blocks" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);" + ] + } + } + ] + }, + { + "name": "Send Reply Message", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/chat/messages", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "chat", + "messages" + ], + "query": [ + + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Replying to a message\",\n \"replyToMessageId\": \"{{replyToMessageId}}\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\"Request succeeded\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201]); });", + "const json = pm.response.json();", + "const value = json.item;", + "if (value) pm.environment.set(\"messageId\", value._id || value.id || value);" + ] + } + } + ] + }, + { + "name": "Send Message Media Upload", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": { + "raw": "{{baseUrl}}/chat/messages/upload", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "chat", + "messages", + "upload" + ], + "query": [ + + ] + }, + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "conversationId", + "value": "{{conversationId}}", + "type": "text" + }, + { + "key": "content", + "value": "Media message caption", + "type": "text" + }, + { + "key": "replyToMessageId", + "value": "{{replyToMessageId}}", + "type": "text" + }, + { + "key": "mediaFile", + "type": "file", + "src": "{{mediaFilePath}}" + } + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\"Request succeeded\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201]); });", + "const json = pm.response.json();", + "const value = json.item;", + "if (value) pm.environment.set(\"messageId\", value._id || value.id || value);" + ] + } + } + ] + }, + { + "name": "React To Message", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/chat/messages/{{messageId}}/reaction", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "chat", + "messages", + "{{messageId}}", + "reaction" + ], + "query": [ + + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"reactionType\": \"love\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "Delete Message For Me", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/chat/messages/{{messageId}}/delete-for-me", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "chat", + "messages", + "{{messageId}}", + "delete-for-me" + ], + "query": [ + + ] + } + } + } + ] + }, + { + "name": "Feed", + "item": [ + { + "name": "Get My Feed Mixed Home", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/feed/me?page=1\u0026limit=20\u0026includeSuggestions={{feedIncludeSuggestions}}\u0026suggestionInterval={{feedSuggestionInterval}}\u0026followingOnly={{feedFollowingOnly}}\u0026radiusKm={{feedRadiusKm}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "if (json.items \u0026\u0026 json.items.length \u003e 0) {", + " pm.expect(json.items[0]).to.have.property(\u0027feedItemType\u0027);", + "}", + "const hasMixedItems = (json.items || []).some(item =\u003e item.feedItemType === \u0027suggested_users\u0027 || item.feedItemType === \u0027featured_marketplace\u0027);", + "pm.environment.set(\u0027homeFeedHasMixedItems\u0027, hasMixedItems ? \u0027true\u0027 : \u0027false\u0027);" + ] + } + } + ] + }, + { + "name": "Get My Feed", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/feed/me?limit=20\u0026cursor={{feedCursor}}\u0026followingOnly={{feedFollowingOnly}}\u0026radiusKm={{feedRadiusKm}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;", + "const firstPost = (json.items || []).find(item =\u003e item.feedItemType === \u0027post\u0027);", + "if (firstPost) {", + " pm.expect(firstPost).to.have.property(\u0027likedByMe\u0027);", + " pm.expect(firstPost).to.have.property(\u0027savedByMe\u0027);", + " pm.expect(firstPost).to.have.property(\u0027engagement\u0027);", + "}", + "pm.environment.set(\u0027feedCursor\u0027, json.nextCursor || \u0027\u0027);" + ] + } + } + ] + }, + { + "name": "Get My Feed Preferred Type", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/feed/me?limit=20\u0026cursor={{feedCursor}}\u0026preferredPostType={{feedPreferredPostType}}\u0026followingOnly={{feedFollowingOnly}}\u0026radiusKm={{feedRadiusKm}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.pagination).to.exist;", + "const firstPost = (json.items || []).find(item =\u003e item.feedItemType === \u0027post\u0027);", + "if (firstPost) { pm.expect(firstPost).to.have.property(\u0027engagement\u0027); }", + "pm.environment.set(\u0027feedCursor\u0027, json.nextCursor || \u0027\u0027);" + ] + } + } + ] + }, + { + "name": "Get Trending Feed", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/feed/trending?limit=20\u0026cursor={{feedCursor}}\u0026preferredPostType={{feedPreferredPostType}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;", + "const firstPost = (json.items || []).find(item =\u003e item.feedItemType === \u0027post\u0027);", + "if (firstPost) {", + " pm.expect(firstPost).to.have.property(\u0027likedByMe\u0027);", + " pm.expect(firstPost).to.have.property(\u0027savedByMe\u0027);", + "}", + "pm.environment.set(\u0027feedCursor\u0027, json.nextCursor || \u0027\u0027);" + ] + } + } + ] + }, + { + "name": "Explore Feed", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/feed/explore?page=1\u0026limit=20" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);" + ] + } + } + ] + } + ] + }, + { + "name": "Smoke", + "item": [ + { + "name": "Login (Smoke)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/login", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"password\": \"StrongPass123!\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "const uid = json.user._id || json.user.id;", + "pm.environment.set(\u0027accessToken\u0027, json.accessToken);", + "pm.environment.set(\u0027refreshToken\u0027, json.refreshToken);", + "pm.environment.set(\u0027userId\u0027, uid);" + ] + } + } + ] + }, + { + "name": "Get User Posts (Smoke)", + "request": { + "method": "GET", + "url": "{{baseUrl}}/posts/user/{{userId}}?page=1\u0026limit=10", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ] + } + }, + { + "name": "Get My Feed (Smoke)", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/feed/me?limit=20\u0026cursor={{feedCursor}}\u0026radiusKm=30" + } + } + ] + }, + { + "name": "Full E2E", + "item": [ + { + "name": "Create Post (Own) - E2E", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "content", + "value": "E2E text post", + "type": "text" + }, + { + "key": "visibility", + "value": "public", + "type": "text" + } + ] + } + } + }, + { + "name": "Create Comment - E2E", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/comments", + "body": { + "mode": "raw", + "raw": "{\n \"postId\": \"{{postId}}\",\n \"content\": \"E2E comment\"\n}" + } + } + }, + { + "name": "Like Post - E2E", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/likes", + "body": { + "mode": "raw", + "raw": "{\n \"targetId\": \"{{postId}}\",\n \"targetType\": \"post\"\n}" + } + } + }, + { + "name": "Save Post - E2E", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/saves", + "body": { + "mode": "raw", + "raw": "{\n \"postId\": \"{{postId}}\"\n}" + } + } + }, + { + "name": "Delete Post - E2E Cleanup", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts/{{postId}}" + } + } + ] + }, + { + "name": "Negative Tests", + "item": [ + { + "name": "Create Invalid Post (Video+Audio) - Negative", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "content", + "value": "Invalid post", + "type": "text" + }, + { + "key": "videoUrl", + "value": "https://cdn.example.com/video.mp4", + "type": "text" + }, + { + "key": "audioUrl", + "value": "https://cdn.example.com/audio.mp3", + "type": "text" + }, + { + "key": "visibility", + "value": "public", + "type": "text" + } + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 400\u0027, function () { pm.response.to.have.status(400); });" + ] + } + } + ] + }, + { + "name": "Create Post Without Token - Negative", + "request": { + "method": "POST", + "header": [ + + ], + "url": "{{baseUrl}}/posts", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "content", + "value": "No token request", + "type": "text" + }, + { + "key": "visibility", + "value": "public", + "type": "text" + } + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 401\u0027, function () { pm.response.to.have.status(401); });" + ] + } + } + ] + } + ] + }, + { + "name": "Cleanup", + "item": [ + { + "name": "Delete Own Comment (if exists)", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/comments/{{ownCommentId}}" + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (!pm.environment.get(\u0027ownCommentId\u0027)) { postman.setNextRequest(\u0027Delete Own Post (if exists)\u0027); }" + ] + } + } + ] + }, + { + "name": "Delete Own Post (if exists)", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts/{{ownPostId}}" + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (!pm.environment.get(\u0027ownPostId\u0027)) { postman.setNextRequest(\u0027Delete Instrument (if exists)\u0027); }" + ] + } + } + ] + } + ] + }, + { + "name": "AI Music", + "item": [ + { + "name": "Generate Music From Text (Gemini)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/media/ai/text-to-music", + "body": { + "mode": "raw", + "raw": "{\n \"prompt\": \"{{aiMusicPrompt}}\",\n \"durationSeconds\": {{aiMusicDuration}},\n \"seed\": {{aiMusicSeed}}\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "if (!pm.environment.get(\u0027aiMusicPrompt\u0027)) { pm.environment.set(\u0027aiMusicPrompt\u0027, \u0027Calm oud melody with light percussion and emotional Arabic mood\u0027); }", + "if (!pm.environment.get(\u0027aiMusicDuration\u0027)) { pm.environment.set(\u0027aiMusicDuration\u0027, \u002712\u0027); }", + "if (!pm.environment.get(\u0027aiMusicSeed\u0027)) { pm.environment.set(\u0027aiMusicSeed\u0027, \u002742\u0027); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.audioUrl).to.exist;", + "pm.expect(json.mimeType).to.exist;", + "pm.expect(json.sizeBytes).to.be.a(\u0027number\u0027);", + "pm.environment.set(\u0027generatedMusicUrl\u0027, json.audioUrl);" + ] + } + } + ] + } + ] + }, + { + "name": "Marketplace", + "item": [ + { + "name": "01 Marketplace Overview", + "item": [ + { + "name": "Public Marketplace Home", + "request": { + "method": "GET", + "header": [ + + ], + "url": "{{baseUrl}}/marketplace/home?listingsLimit=4\u0026instrumentsLimit=4\u0026repairShopsLimit=3\u0026onlyActive=true" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property(\u0027categories\u0027);", + "pm.expect(json).to.have.property(\u0027summary\u0027);", + "pm.expect(json).to.have.property(\u0027featuredShops\u0027);", + "pm.expect(json.filters).to.have.property(\u0027listingCategories\u0027);", + "pm.expect(json.sections).to.have.property(\u0027listings\u0027);", + "pm.expect(json.sections).to.have.property(\u0027musicalInstruments\u0027);", + "pm.expect(json.sections).to.have.property(\u0027repairShops\u0027);" + ] + } + } + ] + } + ] + }, + { + "name": "03 Repair Shops", + "item": [ + { + "name": "Public", + "item": [ + { + "name": "Public List Repair Shops", + "request": { + "method": "GET", + "header": [ + + ], + "url": "{{baseUrl}}/marketplace/repair-shops?q={{repairShopQuery}}\u0026isActive={{repairShopIsActive}}\u0026sortBy={{repairShopSortBy}}\u0026sortOrder={{listSortOrder}}\u0026page=1\u0026limit=20" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;", + "if (json.items \u0026\u0026 json.items.length \u003e 0) { pm.environment.set(\u0027repairShopId\u0027, json.items[0]._id || json.items[0].id); }" + ] + } + } + ] + }, + { + "name": "Public Get Repair Shop By Id", + "request": { + "method": "GET", + "header": [ + + ], + "url": "{{baseUrl}}/marketplace/repair-shops/{{repairShopId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });" + ] + } + } + ] + } + ] + } + ] + }, + { + "name": "04 Musical Instruments", + "item": [ + { + "name": "Public", + "item": [ + { + "name": "Public List Instruments", + "request": { + "method": "GET", + "header": [ + + ], + "url": "{{baseUrl}}/marketplace/instruments?q={{marketplaceQuery}}\u0026minPrice={{marketplaceMinPrice}}\u0026maxPrice={{marketplaceMaxPrice}}\u0026isActive={{marketplaceIsActive}}\u0026condition={{marketplaceCondition}}\u0026instrumentType={{marketplaceInstrumentType}}\u0026sortBy={{marketplaceSortBy}}\u0026sortOrder={{listSortOrder}}\u0026page=1\u0026limit=20" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;", + "if (json.items \u0026\u0026 json.items.length \u003e 0) { pm.environment.set(\u0027instrumentId\u0027, json.items[0]._id || json.items[0].id); }" + ] + } + } + ] + }, + { + "name": "Public Get Instrument By Id", + "request": { + "method": "GET", + "header": [ + + ], + "url": "{{baseUrl}}/marketplace/instruments/{{instrumentId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });" + ] + } + } + ] + } + ] + } + ] + }, + { + "name": "05 General Marketplace", + "item": [ + { + "name": "Public", + "item": [ + { + "name": "Public List Listings", + "request": { + "method": "GET", + "header": [ + + ], + "url": "{{baseUrl}}/marketplace/listings?q={{marketplaceQuery}}\u0026minPrice={{marketplaceMinPrice}}\u0026maxPrice={{marketplaceMaxPrice}}\u0026isActive={{marketplaceIsActive}}\u0026listingCategory={{marketplaceListingCategory}}\u0026condition={{marketplaceCondition}}\u0026instrumentType={{marketplaceInstrumentType}}\u0026sortBy={{marketplaceSortBy}}\u0026sortOrder={{listSortOrder}}\u0026page=1\u0026limit=20" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an(\u0027array\u0027);", + "pm.expect(json.pagination).to.exist;", + "if (json.items \u0026\u0026 json.items.length \u003e 0) { pm.environment.set(\u0027listingId\u0027, json.items[0]._id || json.items[0].id); }" + ] + } + } + ] + }, + { + "name": "Public Get Listing By Id", + "request": { + "method": "GET", + "header": [ + + ], + "url": "{{baseUrl}}/marketplace/listings/{{listingId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });" + ] + } + } + ] + } + ] + } + ] + } + ] + }, + { + "name": "Blocks", + "item": [ + { + "name": "Block User", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/blocks/{{targetUserId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 201\u0027, function () { pm.response.to.have.status(201); });" + ] + } + } + ] + }, + { + "name": "Block Status", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/blocks/status/{{targetUserId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Unblock User", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/blocks/{{targetUserId}}/unblock" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });" + ] + } + } + ] + } + ] + }, + { + "name": "Reports", + "item": [ + { + "name": "Report Post", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/reports", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "reports" + ], + "query": [ + + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"targetType\": \"post\",\n \"targetId\": \"{{postId}}\",\n \"reason\": \"spam\",\n \"details\": \"This content looks suspicious\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\"Request succeeded\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201]); });", + "const json = pm.response.json();", + "const value = json.item;", + "if (value) pm.environment.set(\"reportId\", value._id || value.id || value);" + ] + } + } + ] + }, + { + "name": "My Reports", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/reports/me?page=1\u0026limit=20" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is 200\u0027, function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Report Post - Fixed Reason", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/reports", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "reports" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"targetType\": \"post\",\n \"targetId\": \"{{postId}}\",\n \"reason\": \"{{reportReason}}\",\n \"details\": \"Reported from Postman\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "Report User And Block", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/reports", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "reports" + ], + "query": [ + + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"targetType\": \"user\",\n \"targetId\": \"{{targetUserId}}\",\n \"reason\": \"harassment\",\n \"details\": \"Unsafe interaction\",\n \"blockTarget\": true\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\"Request succeeded\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201]); });", + "const json = pm.response.json();", + "const value = json.item;", + "if (value) pm.environment.set(\"reportId\", value._id || value.id || value);" + ] + } + } + ] + }, + { + "name": "Report Comment", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/reports", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "reports" + ], + "query": [ + + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"targetType\": \"comment\",\n \"targetId\": \"{{commentId}}\",\n \"reason\": \"harassment\",\n \"details\": \"Offensive comment\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\"Request succeeded\", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201]); });", + "const json = pm.response.json();", + "const value = json.item;", + "if (value) pm.environment.set(\"reportId\", value._id || value.id || value);" + ] + } + } + ] + } + ] + }, + { + "name": "Health", + "item": [ + { + "name": "Health Check", + "request": { + "method": "GET", + "header": [ + + ], + "url": { + "raw": "{{baseUrl}}/", + "host": [ + "{{baseUrl}}" + ], + "path": [ + + ] + } + } + } + ] + }, + { + "name": "Devices", + "item": [ + { + "name": "Register Device FCM Token", + "request": { + "method": "POST", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/devices/register", + "body": { + "mode": "raw", + "raw": "{\n \"fcmToken\": \"{{fcmToken}}\",\n \"platform\": \"android\",\n \"deviceId\": \"{{deviceId}}\",\n \"appVersion\": \"1.0.0\",\n \"locale\": \"ar\"\n}" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Device registered\u0027, function () { const json = pm.response.json(); pm.expect(json.message).to.eql(\u0027Device registered successfully\u0027); pm.expect(json).to.have.property(\u0027device\u0027); });" + ] + } + } + }, + { + "name": "Unregister Device FCM Token", + "request": { + "method": "POST", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/devices/unregister", + "body": { + "mode": "raw", + "raw": "{\n \"fcmToken\": \"{{fcmToken}}\"\n}" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Device unregistered\u0027, function () { const json = pm.response.json(); pm.expect(json.message).to.eql(\u0027Device unregistered successfully\u0027); });" + ] + } + } + } + ] + }, + { + "name": "Collaboration Requests", + "item": [ + { + "name": "Create Collaboration Request", + "request": { + "method": "POST", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/posts/{{postId}}/collaboration-requests", + "body": { + "mode": "raw", + "raw": "{\n \"targetUserId\": \"{{targetUserId}}\"\n}" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Collaboration request response\u0027, function () { const json = pm.response.json(); pm.expect(json).to.have.property(\u0027request\u0027); });" + ] + } + } + }, + { + "name": "List My Collaboration Requests", + "request": { + "method": "GET", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/collaboration-requests?page=1\u0026limit=20", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Status is success\u0027, function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204]); });" + ] + } + } + }, + { + "name": "Approve Collaboration Request", + "request": { + "method": "PATCH", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/collaboration-requests/{{collaborationRequestId}}/approve", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Approved\u0027, function () { const json = pm.response.json(); pm.expect(json.approved).to.eql(true); });" + ] + } + } + }, + { + "name": "Reject Collaboration Request", + "request": { + "method": "PATCH", + "header": { + "key": "Content-Type", + "value": "application/json" + }, + "url": "{{baseUrl}}/collaboration-requests/{{collaborationRequestId}}/reject", + "body": { + "mode": "raw", + "raw": "" + } + }, + "event": { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\u0027Rejected\u0027, function () { const json = pm.response.json(); pm.expect(json.rejected).to.eql(true); });" + ] + } + } + } + ] + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:4000/api/v1", + "type": "string" + }, + { + "key": "accessToken", + "value": "", + "type": "string" + }, + { + "key": "refreshToken", + "value": "", + "type": "string" + }, + { + "key": "superAdminAccessToken", + "value": "", + "type": "string" + }, + { + "key": "superAdminRefreshToken", + "value": "", + "type": "string" + }, + { + "key": "targetAccessToken", + "value": "", + "type": "string" + }, + { + "key": "targetRefreshToken", + "value": "", + "type": "string" + }, + { + "key": "userId", + "value": "", + "type": "string" + }, + { + "key": "currentUserId", + "value": "", + "type": "string" + }, + { + "key": "targetUserId", + "value": "", + "type": "string" + }, + { + "key": "targetUsername", + "value": "", + "type": "string" + }, + { + "key": "lastFollowingState", + "value": "", + "type": "string" + }, + { + "key": "notificationUnreadCount", + "value": "", + "type": "string" + }, + { + "key": "notificationId", + "value": "", + "type": "string" + }, + { + "key": "postId", + "value": "", + "type": "string" + }, + { + "key": "ownPostId", + "value": "", + "type": "string" + }, + { + "key": "targetPostId", + "value": "", + "type": "string" + }, + { + "key": "adminPostId", + "value": "", + "type": "string" + }, + { + "key": "commentId", + "value": "", + "type": "string" + }, + { + "key": "ownCommentId", + "value": "", + "type": "string" + }, + { + "key": "replyCommentId", + "value": "", + "type": "string" + }, + { + "key": "conversationId", + "value": "", + "type": "string" + }, + { + "key": "messageId", + "value": "", + "type": "string" + }, + { + "key": "sessionJti", + "value": "", + "type": "string" + }, + { + "key": "registerEmail", + "value": "", + "type": "string" + }, + { + "key": "registerUsername", + "value": "", + "type": "string" + }, + { + "key": "adminUserId", + "value": "", + "type": "string" + }, + { + "key": "targetLoginEmail", + "value": "", + "type": "string" + }, + { + "key": "targetLoginPassword", + "value": "", + "type": "string" + }, + { + "key": "resetCode", + "value": "", + "type": "string" + }, + { + "key": "resetToken", + "value": "", + "type": "string" + }, + { + "key": "emailVerificationCode", + "value": "", + "type": "string" + }, + { + "key": "googleIdToken", + "value": "", + "type": "string" + }, + { + "key": "newPassword", + "value": "NewStrongPass123!", + "type": "string" + }, + { + "key": "feedCursor", + "value": "", + "type": "string" + }, + { + "key": "chatConversationsCursor", + "value": "", + "type": "string" + }, + { + "key": "chatMessagesCursor", + "value": "", + "type": "string" + }, + { + "key": "adminAccessToken", + "value": "", + "type": "string" + }, + { + "key": "adminRefreshToken", + "value": "", + "type": "string" + }, + { + "key": "adminEmail", + "value": "store_admin@example.com", + "type": "string" + }, + { + "key": "adminUsername", + "value": "store_admin_01", + "type": "string" + }, + { + "key": "adminPassword", + "value": "AdminStrongPass123!", + "type": "string" + }, + { + "key": "usersQuery", + "value": "user", + "type": "string" + }, + { + "key": "usersVerified", + "value": "true", + "type": "string" + }, + { + "key": "usersSortBy", + "value": "createdAt", + "type": "string" + }, + { + "key": "listSortOrder", + "value": "desc", + "type": "string" + }, + { + "key": "commentSortBy", + "value": "createdAt", + "type": "string" + }, + { + "key": "postVisibility", + "value": "public", + "type": "string" + }, + { + "key": "postSortBy", + "value": "createdAt", + "type": "string" + }, + { + "key": "postTypeFilter", + "value": "image", + "type": "string" + }, + { + "key": "postSearchQuery", + "value": "music", + "type": "string" + }, + { + "key": "postHashtag", + "value": "music", + "type": "string" + }, + { + "key": "reelQuery", + "value": "reel", + "type": "string" + }, + { + "key": "notificationRead", + "value": "false", + "type": "string" + }, + { + "key": "notificationType", + "value": "mention", + "type": "string" + }, + { + "key": "notificationResourceType", + "value": "post", + "type": "string" + }, + { + "key": "feedPreferredPostType", + "value": "video", + "type": "string" + }, + { + "key": "feedIncludeSuggestions", + "value": "true", + "type": "string" + }, + { + "key": "feedSuggestionInterval", + "value": "4", + "type": "string" + }, + { + "key": "feedFollowingOnly", + "value": "false", + "type": "string" + }, + { + "key": "feedRadiusKm", + "value": "30", + "type": "string" + }, + { + "key": "homeFeedHasMixedItems", + "value": "", + "type": "string" + }, + { + "key": "lastViewCount", + "value": "", + "type": "string" + }, + { + "key": "lastPlayCount", + "value": "", + "type": "string" + }, + { + "key": "lastShareCount", + "value": "", + "type": "string" + }, + { + "key": "marketplaceQuery", + "value": "oud", + "type": "string" + }, + { + "key": "marketplaceMinPrice", + "value": "1000", + "type": "string" + }, + { + "key": "marketplaceMaxPrice", + "value": "5000", + "type": "string" + }, + { + "key": "marketplaceIsActive", + "value": "true", + "type": "string" + }, + { + "key": "marketplaceSortBy", + "value": "price", + "type": "string" + }, + { + "key": "repairShopQuery", + "value": "riyadh", + "type": "string" + }, + { + "key": "repairShopIsActive", + "value": "true", + "type": "string" + }, + { + "key": "repairShopSortBy", + "value": "name", + "type": "string" + }, + { + "key": "instrumentId", + "value": "", + "type": "string" + }, + { + "key": "repairShopId", + "value": "", + "type": "string" + }, + { + "key": "generatedMusicUrl", + "value": "", + "type": "string" + }, + { + "key": "aiMusicPrompt", + "value": "Calm oud melody with light percussion and emotional Arabic mood", + "type": "string" + }, + { + "key": "aiMusicDuration", + "value": "12", + "type": "string" + }, + { + "key": "aiMusicSeed", + "value": "42", + "type": "string" + }, + { + "key": "listingId", + "value": "", + "type": "string" + }, + { + "key": "marketplaceListingCategory", + "value": "accessory", + "type": "string" + }, + { + "key": "marketplaceCondition", + "value": "used", + "type": "string" + }, + { + "key": "marketplaceInstrumentType", + "value": "oud", + "type": "string" + }, + { + "key": "talentRole", + "value": "instrumentalist", + "type": "string" + }, + { + "key": "talentExperienceLevel", + "value": "", + "type": "string" + }, + { + "key": "usersHasAvatar", + "value": "true", + "type": "string" + }, + { + "key": "reactionType", + "value": "love", + "type": "string" + }, + { + "key": "followRequestId", + "value": "", + "type": "string" + }, + { + "key": "reportId", + "value": "", + "type": "string" + }, + { + "key": "repostId", + "value": "", + "type": "string" + }, + { + "key": "quotePostId", + "value": "", + "type": "string" + }, + { + "key": "replyToMessageId", + "value": "" + }, + { + "key": "mediaFilePath", + "value": "" + }, + { + "key": "collaboratorId", + "value": "" + }, + { + "key": "commentKeyword", + "value": "spam" + }, + { + "key": "reportReason", + "value": "spam" + }, + { + "key": "sessionId", + "value": "" + }, + { + "key": "caseId", + "value": "" + }, + { + "key": "historyId", + "value": "" + }, + { + "key": "bulkActionTargetId", + "value": "" + }, + { + "key": "auditAction", + "value": "" + } + ] } diff --git a/src/app.module.ts b/src/app.module.ts index e9827b6..af513a5 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -16,6 +16,8 @@ import { AuditModule } from './modules/audit/audit.module'; import { BlocksModule } from './modules/blocks/blocks.module'; import { ChatModule } from './modules/chat/chat.module'; import { CommentsModule } from './modules/comments/comments.module'; +import { CollaborationRequestsModule } from './modules/collaboration-requests/collaboration-requests.module'; +import { DevicesModule } from './modules/devices/devices.module'; import { FeedModule } from './modules/feed/feed.module'; import { FollowsModule } from './modules/follows/follows.module'; import { LikesModule } from './modules/likes/likes.module'; @@ -50,6 +52,8 @@ import { ThrottleGuard } from './common/guards/throttle.guard'; AuthModule, PostsModule, CommentsModule, + CollaborationRequestsModule, + DevicesModule, LikesModule, FollowsModule, FeedModule, diff --git a/src/common/enums/notification-type.enum.ts b/src/common/enums/notification-type.enum.ts index 2e8bbed..70fc607 100644 --- a/src/common/enums/notification-type.enum.ts +++ b/src/common/enums/notification-type.enum.ts @@ -6,4 +6,7 @@ export enum NotificationType { SAVE = 'save', SHARE = 'share', MENTION = 'mention', + REPLY = 'reply', + SYSTEM = 'system', + COLLABORATION_REQUEST = 'collaboration_request', } diff --git a/src/modules/chat/chat.gateway.ts b/src/modules/chat/chat.gateway.ts index 5cbe4fb..ec4923a 100644 --- a/src/modules/chat/chat.gateway.ts +++ b/src/modules/chat/chat.gateway.ts @@ -14,6 +14,7 @@ import { Server, Socket } from 'socket.io'; import { ChatRealtimeService } from './chat-realtime.service'; import { ChatService } from './chat.service'; import { SendMessageDto } from './dto/send-message.dto'; +import { UsersService } from '../users/users.service'; type SocketWithUser = Socket & { data: { userId?: string } }; @@ -21,12 +22,14 @@ type SocketWithUser = Socket & { data: { userId?: string } }; export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server!: Server; + private readonly connectionCountsByUser = new Map(); constructor( private readonly chatService: ChatService, private readonly chatRealtimeService: ChatRealtimeService, private readonly jwtService: JwtService, private readonly configService: ConfigService, + private readonly usersService: UsersService, ) {} afterInit(server: Server) { @@ -49,6 +52,8 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa return; } client.data.userId = payload.sub; + this.incrementUserConnection(payload.sub); + await this.usersService.setPresence(payload.sub, true); await client.join(this.userRoom(payload.sub)); this.server.to(this.userRoom(payload.sub)).emit('presence', { userId: payload.sub, online: true }); } catch { @@ -59,7 +64,11 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa handleDisconnect(client: SocketWithUser) { const userId = client.data.userId; if (userId) { - this.server.to(this.userRoom(userId)).emit('presence', { userId, online: false }); + const remainingConnections = this.decrementUserConnection(userId); + if (remainingConnections === 0) { + void this.usersService.setPresence(userId, false); + this.server.to(this.userRoom(userId)).emit('presence', { userId, online: false }); + } } } @@ -140,4 +149,18 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa private conversationRoom(conversationId: string): string { return `conversation:${conversationId}`; } + + private incrementUserConnection(userId: string): void { + this.connectionCountsByUser.set(userId, (this.connectionCountsByUser.get(userId) ?? 0) + 1); + } + + private decrementUserConnection(userId: string): number { + const nextCount = Math.max(0, (this.connectionCountsByUser.get(userId) ?? 1) - 1); + if (nextCount === 0) { + this.connectionCountsByUser.delete(userId); + return 0; + } + this.connectionCountsByUser.set(userId, nextCount); + return nextCount; + } } diff --git a/src/modules/collaboration-requests/collaboration-requests.controller.ts b/src/modules/collaboration-requests/collaboration-requests.controller.ts new file mode 100644 index 0000000..74ae6ce --- /dev/null +++ b/src/modules/collaboration-requests/collaboration-requests.controller.ts @@ -0,0 +1,39 @@ +import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { PaginationQueryDto } from '../../common/dto/pagination-query.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { JwtPayload } from '../../common/interfaces/jwt-payload.interface'; +import { CollaborationRequestsService } from './collaboration-requests.service'; +import { CreateCollaborationRequestDto } from './dto/create-collaboration-request.dto'; + +@ApiTags('Collaboration Requests') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) +export class CollaborationRequestsController { + constructor(private readonly collaborationRequestsService: CollaborationRequestsService) {} + + @Post('posts/:postId/collaboration-requests') + async create( + @CurrentUser() user: JwtPayload, + @Param('postId') postId: string, + @Body() dto: CreateCollaborationRequestDto, + ) { + return this.collaborationRequestsService.create(user.sub, postId, dto.targetUserId); + } + + @Get('collaboration-requests') + async mine(@CurrentUser() user: JwtPayload, @Query() query: PaginationQueryDto) { + return this.collaborationRequestsService.getMine(user.sub, query); + } + + @Patch('collaboration-requests/:requestId/approve') + async approve(@CurrentUser() user: JwtPayload, @Param('requestId') requestId: string) { + return this.collaborationRequestsService.approve(user.sub, requestId); + } + + @Patch('collaboration-requests/:requestId/reject') + async reject(@CurrentUser() user: JwtPayload, @Param('requestId') requestId: string) { + return this.collaborationRequestsService.reject(user.sub, requestId); + } +} diff --git a/src/modules/collaboration-requests/collaboration-requests.module.ts b/src/modules/collaboration-requests/collaboration-requests.module.ts new file mode 100644 index 0000000..d6e046c --- /dev/null +++ b/src/modules/collaboration-requests/collaboration-requests.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { BlocksModule } from '../blocks/blocks.module'; +import { NotificationsModule } from '../notifications/notifications.module'; +import { PostsModule } from '../posts/posts.module'; +import { UsersModule } from '../users/users.module'; +import { CollaborationRequestsController } from './collaboration-requests.controller'; +import { CollaborationRequestsService } from './collaboration-requests.service'; +import { + CollaborationRequest, + CollaborationRequestSchema, +} from './schemas/collaboration-request.schema'; + +@Module({ + imports: [ + BlocksModule, + NotificationsModule, + PostsModule, + UsersModule, + MongooseModule.forFeature([{ name: CollaborationRequest.name, schema: CollaborationRequestSchema }]), + ], + controllers: [CollaborationRequestsController], + providers: [CollaborationRequestsService], +}) +export class CollaborationRequestsModule {} diff --git a/src/modules/collaboration-requests/collaboration-requests.service.ts b/src/modules/collaboration-requests/collaboration-requests.service.ts new file mode 100644 index 0000000..be7d974 --- /dev/null +++ b/src/modules/collaboration-requests/collaboration-requests.service.ts @@ -0,0 +1,136 @@ +import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model, Types } from 'mongoose'; +import { PaginationQueryDto } from '../../common/dto/pagination-query.dto'; +import { buildPaginatedResponse } from '../../common/utils/pagination.util'; +import { BlocksRepository } from '../blocks/blocks.repository'; +import { NotificationsService } from '../notifications/notifications.service'; +import { PostsRepository } from '../posts/posts.repository'; +import { UsersRepository } from '../users/users.repository'; +import { + CollaborationRequest, + CollaborationRequestDocument, +} from './schemas/collaboration-request.schema'; + +@Injectable() +export class CollaborationRequestsService { + constructor( + @InjectModel(CollaborationRequest.name) + private readonly collaborationRequestModel: Model, + private readonly postsRepository: PostsRepository, + private readonly usersRepository: UsersRepository, + private readonly blocksRepository: BlocksRepository, + private readonly notificationsService: NotificationsService, + ) {} + + async create(requesterId: string, postId: string, targetUserId: string) { + if (!Types.ObjectId.isValid(postId) || !Types.ObjectId.isValid(targetUserId)) { + 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), + ]); + if (!post) { + throw new NotFoundException('Post not found'); + } + if (post.authorId.toString() !== requesterId) { + throw new ForbiddenException('Only the post owner can invite collaborators'); + } + if (!targetUser || targetUser.isDisabled) { + throw new NotFoundException('Target user not found'); + } + if (block) { + throw new BadRequestException('You cannot invite this user'); + } + + const filter = { + postId: new Types.ObjectId(postId), + requesterId: new Types.ObjectId(requesterId), + targetUserId: new Types.ObjectId(targetUserId), + status: 'pending', + }; + const existing = await this.collaborationRequestModel.findOne(filter).exec(); + const request = existing ?? await this.collaborationRequestModel + .findOneAndUpdate( + filter, + { + $setOnInsert: { + postId: new Types.ObjectId(postId), + requesterId: new Types.ObjectId(requesterId), + targetUserId: new Types.ObjectId(targetUserId), + status: 'pending', + }, + }, + { new: true, upsert: true, setDefaultsOnInsert: true }, + ) + .exec(); + + if (!existing) { + await this.notificationsService.create({ + actorId: requesterId, + recipientId: targetUserId, + type: 'collaboration_request', + referenceId: postId, + resourceType: 'post', + deepLink: `/posts/${postId}`, + }); + } + + return { message: 'Collaboration request sent', request }; + } + + async getMine(targetUserId: string, query: PaginationQueryDto) { + const page = query.page ?? 1; + const limit = query.limit ?? 20; + const skip = (page - 1) * limit; + const filter = { targetUserId: new Types.ObjectId(targetUserId), status: 'pending' }; + const [items, total] = await Promise.all([ + this.collaborationRequestModel + .find(filter) + .populate({ path: 'requesterId', select: 'name username stageName avatar isVerified isDisabled' }) + .populate({ path: 'postId' }) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit) + .exec(), + this.collaborationRequestModel.countDocuments(filter).exec(), + ]); + return buildPaginatedResponse(items, { page, limit, total, offset: skip }); + } + + async approve(targetUserId: string, requestId: string) { + const request = await this.updateStatus(targetUserId, requestId, 'approved'); + await this.postsRepository.updateById(request.postId.toString(), { + $addToSet: { collaboratorIds: request.targetUserId }, + }); + return { approved: true, request }; + } + + async reject(targetUserId: string, requestId: string) { + const request = await this.updateStatus(targetUserId, requestId, 'rejected'); + return { rejected: true, request }; + } + + private async updateStatus(targetUserId: string, requestId: string, status: 'approved' | 'rejected') { + if (!Types.ObjectId.isValid(requestId)) { + throw new BadRequestException('Invalid collaboration request id'); + } + const request = await this.collaborationRequestModel + .findOneAndUpdate( + { _id: new Types.ObjectId(requestId), targetUserId: new Types.ObjectId(targetUserId), status: 'pending' }, + { status }, + { new: true }, + ) + .exec(); + if (!request) { + throw new NotFoundException('Collaboration request not found'); + } + return request; + } +} diff --git a/src/modules/collaboration-requests/dto/create-collaboration-request.dto.ts b/src/modules/collaboration-requests/dto/create-collaboration-request.dto.ts new file mode 100644 index 0000000..6af9d68 --- /dev/null +++ b/src/modules/collaboration-requests/dto/create-collaboration-request.dto.ts @@ -0,0 +1,6 @@ +import { IsMongoId } from 'class-validator'; + +export class CreateCollaborationRequestDto { + @IsMongoId() + targetUserId!: string; +} diff --git a/src/modules/collaboration-requests/schemas/collaboration-request.schema.ts b/src/modules/collaboration-requests/schemas/collaboration-request.schema.ts new file mode 100644 index 0000000..8594878 --- /dev/null +++ b/src/modules/collaboration-requests/schemas/collaboration-request.schema.ts @@ -0,0 +1,25 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, Types } from 'mongoose'; +import { Post } from '../../posts/schemas/post.schema'; +import { User } from '../../users/schemas/user.schema'; + +export type CollaborationRequestDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class CollaborationRequest { + @Prop({ type: Types.ObjectId, ref: Post.name, required: true, index: true }) + postId!: Types.ObjectId; + + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + requesterId!: Types.ObjectId; + + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + targetUserId!: Types.ObjectId; + + @Prop({ enum: ['pending', 'approved', 'rejected'], default: 'pending', index: true }) + status!: 'pending' | 'approved' | 'rejected'; +} + +export const CollaborationRequestSchema = SchemaFactory.createForClass(CollaborationRequest); +CollaborationRequestSchema.index({ postId: 1, targetUserId: 1, status: 1 }); +CollaborationRequestSchema.index({ targetUserId: 1, status: 1, createdAt: -1 }); diff --git a/src/modules/devices/devices.controller.ts b/src/modules/devices/devices.controller.ts new file mode 100644 index 0000000..90ca3cc --- /dev/null +++ b/src/modules/devices/devices.controller.ts @@ -0,0 +1,26 @@ +import { Body, Controller, Post, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { JwtPayload } from '../../common/interfaces/jwt-payload.interface'; +import { RegisterDeviceDto } from './dto/register-device.dto'; +import { UnregisterDeviceDto } from './dto/unregister-device.dto'; +import { DevicesService } from './devices.service'; + +@ApiTags('Devices') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) +@Controller('devices') +export class DevicesController { + constructor(private readonly devicesService: DevicesService) {} + + @Post('register') + async register(@CurrentUser() user: JwtPayload, @Body() dto: RegisterDeviceDto) { + return this.devicesService.register(user.sub, dto); + } + + @Post('unregister') + async unregister(@CurrentUser() user: JwtPayload, @Body() dto: UnregisterDeviceDto) { + return this.devicesService.unregister(user.sub, dto); + } +} diff --git a/src/modules/devices/devices.module.ts b/src/modules/devices/devices.module.ts new file mode 100644 index 0000000..e1a11cc --- /dev/null +++ b/src/modules/devices/devices.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { DevicesController } from './devices.controller'; +import { DevicesRepository } from './devices.repository'; +import { DevicesService } from './devices.service'; +import { Device, DeviceSchema } from './schemas/device.schema'; + +@Module({ + imports: [MongooseModule.forFeature([{ name: Device.name, schema: DeviceSchema }])], + controllers: [DevicesController], + providers: [DevicesService, DevicesRepository], + exports: [DevicesService, DevicesRepository], +}) +export class DevicesModule {} diff --git a/src/modules/devices/devices.repository.ts b/src/modules/devices/devices.repository.ts new file mode 100644 index 0000000..12867aa --- /dev/null +++ b/src/modules/devices/devices.repository.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model, Types } from 'mongoose'; +import { Device, DeviceDocument } from './schemas/device.schema'; + +@Injectable() +export class DevicesRepository { + constructor(@InjectModel(Device.name) private readonly deviceModel: Model) {} + + async upsert(userId: string, payload: Omit, 'userId'>): Promise { + const filter = payload.deviceId + ? { userId: new Types.ObjectId(userId), deviceId: payload.deviceId } + : { userId: new Types.ObjectId(userId), fcmToken: payload.fcmToken }; + + return this.deviceModel + .findOneAndUpdate( + filter, + { + $set: { + ...payload, + userId: new Types.ObjectId(userId), + isActive: true, + lastSeenAt: new Date(), + }, + }, + { new: true, upsert: true, setDefaultsOnInsert: true }, + ) + .exec(); + } + + async deactivate(userId: string, payload: { fcmToken?: string; deviceId?: string }): Promise { + const filter: Record = { userId: new Types.ObjectId(userId) }; + if (payload.deviceId) { + filter.deviceId = payload.deviceId; + } else if (payload.fcmToken) { + filter.fcmToken = payload.fcmToken; + } else { + return null; + } + + return this.deviceModel + .findOneAndUpdate(filter, { isActive: false, lastSeenAt: new Date() }, { new: true }) + .exec(); + } +} diff --git a/src/modules/devices/devices.service.ts b/src/modules/devices/devices.service.ts new file mode 100644 index 0000000..8e4ef06 --- /dev/null +++ b/src/modules/devices/devices.service.ts @@ -0,0 +1,45 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { RegisterDeviceDto } from './dto/register-device.dto'; +import { UnregisterDeviceDto } from './dto/unregister-device.dto'; +import { DevicesRepository } from './devices.repository'; + +@Injectable() +export class DevicesService { + constructor(private readonly devicesRepository: DevicesRepository) {} + + async register(userId: string, dto: RegisterDeviceDto) { + const fcmToken = dto.fcmToken.trim(); + if (!fcmToken) { + throw new BadRequestException('fcmToken is required'); + } + + const device = await this.devicesRepository.upsert(userId, { + fcmToken, + platform: dto.platform, + deviceId: dto.deviceId?.trim() ?? '', + appVersion: dto.appVersion?.trim() ?? '', + locale: dto.locale?.trim() ?? '', + }); + + return { + message: 'Device registered successfully', + device, + }; + } + + async unregister(userId: string, dto: UnregisterDeviceDto) { + if (!dto.deviceId?.trim() && !dto.fcmToken?.trim()) { + throw new BadRequestException('deviceId or fcmToken is required'); + } + + const device = await this.devicesRepository.deactivate(userId, { + deviceId: dto.deviceId?.trim(), + fcmToken: dto.fcmToken?.trim(), + }); + + return { + message: 'Device unregistered successfully', + device, + }; + } +} diff --git a/src/modules/devices/dto/register-device.dto.ts b/src/modules/devices/dto/register-device.dto.ts new file mode 100644 index 0000000..60d9d39 --- /dev/null +++ b/src/modules/devices/dto/register-device.dto.ts @@ -0,0 +1,25 @@ +import { IsEnum, IsOptional, IsString, MaxLength } from 'class-validator'; + +export class RegisterDeviceDto { + @IsString() + @MaxLength(4096) + fcmToken!: string; + + @IsEnum(['android', 'ios', 'web']) + platform!: 'android' | 'ios' | 'web'; + + @IsOptional() + @IsString() + @MaxLength(160) + deviceId?: string; + + @IsOptional() + @IsString() + @MaxLength(60) + appVersion?: string; + + @IsOptional() + @IsString() + @MaxLength(20) + locale?: string; +} diff --git a/src/modules/devices/dto/unregister-device.dto.ts b/src/modules/devices/dto/unregister-device.dto.ts new file mode 100644 index 0000000..2e237c2 --- /dev/null +++ b/src/modules/devices/dto/unregister-device.dto.ts @@ -0,0 +1,13 @@ +import { IsOptional, IsString, MaxLength } from 'class-validator'; + +export class UnregisterDeviceDto { + @IsOptional() + @IsString() + @MaxLength(4096) + fcmToken?: string; + + @IsOptional() + @IsString() + @MaxLength(160) + deviceId?: string; +} diff --git a/src/modules/devices/schemas/device.schema.ts b/src/modules/devices/schemas/device.schema.ts new file mode 100644 index 0000000..4a0cb82 --- /dev/null +++ b/src/modules/devices/schemas/device.schema.ts @@ -0,0 +1,36 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, Types } from 'mongoose'; +import { User } from '../../users/schemas/user.schema'; + +export type DeviceDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class Device { + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + userId!: Types.ObjectId; + + @Prop({ required: true, trim: true, index: true }) + fcmToken!: string; + + @Prop({ enum: ['android', 'ios', 'web'], required: true, index: true }) + platform!: 'android' | 'ios' | 'web'; + + @Prop({ default: '', trim: true, index: true }) + deviceId!: string; + + @Prop({ default: '', trim: true }) + appVersion!: string; + + @Prop({ default: '', trim: true }) + locale!: string; + + @Prop({ default: true, index: true }) + isActive!: boolean; + + @Prop({ type: Date, default: null }) + lastSeenAt?: Date | null; +} + +export const DeviceSchema = SchemaFactory.createForClass(Device); +DeviceSchema.index({ userId: 1, fcmToken: 1 }, { unique: true }); +DeviceSchema.index({ userId: 1, deviceId: 1 }, { sparse: true }); diff --git a/src/modules/follows/follows-users.controller.ts b/src/modules/follows/follows-users.controller.ts new file mode 100644 index 0000000..b316aa3 --- /dev/null +++ b/src/modules/follows/follows-users.controller.ts @@ -0,0 +1,58 @@ +import { Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { PaginationQueryDto } from '../../common/dto/pagination-query.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { JwtPayload } from '../../common/interfaces/jwt-payload.interface'; +import { FollowsService } from './follows.service'; + +@ApiTags('Users') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) +@Controller('users') +export class FollowsUsersController { + constructor(private readonly followsService: FollowsService) {} + + @Get('me/followers') + async myFollowers(@CurrentUser() user: JwtPayload, @Query() query: PaginationQueryDto) { + return this.followsService.getFollowers(user.sub, query, user.sub); + } + + @Get('me/following') + async myFollowing(@CurrentUser() user: JwtPayload, @Query() query: PaginationQueryDto) { + return this.followsService.getFollowing(user.sub, query, user.sub); + } + + @Post(':userId/follow') + async followUser(@CurrentUser() user: JwtPayload, @Param('userId') targetUserId: string) { + return this.followsService.followUser(user.sub, targetUserId); + } + + @Delete(':userId/follow') + async unfollowUser(@CurrentUser() user: JwtPayload, @Param('userId') targetUserId: string) { + return this.followsService.unfollowUser(user.sub, targetUserId); + } + + @Get(':userId/follow-status') + async followStatus(@CurrentUser() user: JwtPayload, @Param('userId') targetUserId: string) { + return this.followsService.getFollowStatus(user.sub, targetUserId); + } + + @Get(':userId/followers') + async followers( + @CurrentUser() user: JwtPayload, + @Param('userId') targetUserId: string, + @Query() query: PaginationQueryDto, + ) { + return this.followsService.getFollowers(targetUserId, query, user.sub); + } + + @Get(':userId/following') + async following( + @CurrentUser() user: JwtPayload, + @Param('userId') targetUserId: string, + @Query() query: PaginationQueryDto, + ) { + return this.followsService.getFollowing(targetUserId, query, user.sub); + } +} diff --git a/src/modules/follows/follows.controller.ts b/src/modules/follows/follows.controller.ts index 6d8638a..332b855 100644 --- a/src/modules/follows/follows.controller.ts +++ b/src/modules/follows/follows.controller.ts @@ -24,15 +24,23 @@ export class FollowsController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('followers/:userId') - async followers(@Param('userId') userId: string, @Query() query: PaginationQueryDto) { - return this.followsService.getFollowers(userId, query); + async followers( + @CurrentUser() user: JwtPayload, + @Param('userId') userId: string, + @Query() query: PaginationQueryDto, + ) { + return this.followsService.getFollowers(userId, query, user.sub); } @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('following/:userId') - async following(@Param('userId') userId: string, @Query() query: PaginationQueryDto) { - return this.followsService.getFollowing(userId, query); + async following( + @CurrentUser() user: JwtPayload, + @Param('userId') userId: string, + @Query() query: PaginationQueryDto, + ) { + return this.followsService.getFollowing(userId, query, user.sub); } @ApiBearerAuth() diff --git a/src/modules/follows/follows.module.ts b/src/modules/follows/follows.module.ts index 24fbeb8..c43dd76 100644 --- a/src/modules/follows/follows.module.ts +++ b/src/modules/follows/follows.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; +import { BlocksModule } from '../blocks/blocks.module'; import { OutboxModule } from '../outbox/outbox.module'; import { UsersModule } from '../users/users.module'; import { FollowsController } from './follows.controller'; +import { FollowsUsersController } from './follows-users.controller'; import { FollowsService } from './follows.service'; import { FollowsRepository } from './follows.repository'; import { FollowRequest, FollowRequestSchema } from './schemas/follow-request.schema'; @@ -10,6 +12,7 @@ import { Follow, FollowSchema } from './schemas/follow.schema'; @Module({ imports: [ + BlocksModule, UsersModule, OutboxModule, MongooseModule.forFeature([ @@ -23,7 +26,7 @@ import { Follow, FollowSchema } from './schemas/follow.schema'; }, ]), ], - controllers: [FollowsController], + controllers: [FollowsController, FollowsUsersController], providers: [FollowsService, FollowsRepository], exports: [FollowsService, FollowsRepository], }) diff --git a/src/modules/follows/follows.repository.ts b/src/modules/follows/follows.repository.ts index d89fbb4..03963e8 100644 --- a/src/modules/follows/follows.repository.ts +++ b/src/modules/follows/follows.repository.ts @@ -39,6 +39,15 @@ export class FollowsRepository { return follow; } + isDuplicateKeyError(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + 'code' in error && + (error as { code?: number }).code === 11000 + ); + } + async deleteById(id: string, session?: ClientSession): Promise { await this.followModel.findByIdAndDelete(id, { session }).exec(); } @@ -83,6 +92,28 @@ export class FollowsRepository { .exec(); } + async findFollowerUserIds(followingId: string, skip: number, limit: number, sort: Record) { + return this.followModel + .find({ followingId: new Types.ObjectId(followingId) }) + .select({ followerId: 1 }) + .sort(sort) + .skip(skip) + .limit(limit) + .lean() + .exec(); + } + + async findFollowingUserIds(followerId: string, skip: number, limit: number, sort: Record) { + return this.followModel + .find({ followerId: new Types.ObjectId(followerId) }) + .select({ followingId: 1 }) + .sort(sort) + .skip(skip) + .limit(limit) + .lean() + .exec(); + } + async upsertPendingRequest(requesterId: string, targetUserId: string): Promise { return this.followRequestModel .findOneAndUpdate( diff --git a/src/modules/follows/follows.service.spec.ts b/src/modules/follows/follows.service.spec.ts index d628e62..4b919e4 100644 --- a/src/modules/follows/follows.service.spec.ts +++ b/src/modules/follows/follows.service.spec.ts @@ -21,12 +21,16 @@ describe('FollowsService', () => { const outboxService = { enqueueFollowNotification: jest.fn().mockRejectedValue(new Error('socket down')), }; + const blocksRepository = { + findAnyBetween: jest.fn().mockResolvedValue(null), + }; const service = new FollowsService( followsRepository as any, usersRepository as any, outboxService as any, { bumpGlobalVersion: jest.fn().mockResolvedValue(1) } as any, + blocksRepository as any, ); await expect(service.toggleFollow(currentUserId, { targetUserId })).resolves.toEqual({ @@ -40,4 +44,51 @@ describe('FollowsService', () => { 'follow-1', ); }); + + it('prevents users from following themselves', async () => { + const userId = '507f1f77bcf86cd799439011'; + const service = new FollowsService( + {} as any, + {} as any, + {} as any, + { bumpGlobalVersion: jest.fn() } as any, + { findAnyBetween: jest.fn() } as any, + ); + + await expect(service.followUser(userId, userId)).rejects.toThrow('You cannot follow yourself'); + }); + + it('returns already-following response without creating duplicate notification', async () => { + const currentUserId = '507f1f77bcf86cd799439011'; + const targetUserId = '507f191e810c19729de860ea'; + const followsRepository = { + findOne: jest.fn().mockResolvedValue({ id: 'existing-follow' }), + count: jest.fn().mockResolvedValueOnce(1).mockResolvedValueOnce(2), + create: jest.fn(), + }; + const usersRepository = { + findById: jest.fn().mockResolvedValue({ id: targetUserId, isDisabled: false, isPrivate: false }), + setFollowingCount: jest.fn(), + setFollowersCount: jest.fn(), + }; + const outboxService = { + enqueueFollowNotification: jest.fn(), + }; + const service = new FollowsService( + followsRepository as any, + usersRepository as any, + outboxService as any, + { bumpGlobalVersion: jest.fn() } as any, + { findAnyBetween: jest.fn().mockResolvedValue(null) } as any, + ); + + await expect(service.followUser(currentUserId, targetUserId)).resolves.toMatchObject({ + message: 'Already following user', + isFollowing: true, + followersCount: 2, + followingCount: 1, + }); + expect(followsRepository.create).not.toHaveBeenCalled(); + expect(outboxService.enqueueFollowNotification).not.toHaveBeenCalled(); + }); }); diff --git a/src/modules/follows/follows.service.ts b/src/modules/follows/follows.service.ts index 75c2b5d..6460c5a 100644 --- a/src/modules/follows/follows.service.ts +++ b/src/modules/follows/follows.service.ts @@ -4,6 +4,7 @@ import { PaginationQueryDto } from '../../common/dto/pagination-query.dto'; import { buildPaginatedResponse } from '../../common/utils/pagination.util'; import { resolveMongoSortDirection } from '../../common/utils/sort.util'; import { FeedVersionService } from '../../infrastructure/cache/feed-version.service'; +import { BlocksRepository } from '../blocks/blocks.repository'; import { OutboxService } from '../outbox/outbox.service'; import { UsersRepository } from '../users/users.repository'; import { UserDocument } from '../users/schemas/user.schema'; @@ -19,23 +20,19 @@ export class FollowsService { private readonly usersRepository: UsersRepository, private readonly outboxService: OutboxService, private readonly feedVersionService: FeedVersionService, + private readonly blocksRepository: BlocksRepository, ) {} async toggleFollow(currentUserId: string, dto: ToggleFollowDto) { const targetUserId = dto.targetUserId; - if (!Types.ObjectId.isValid(targetUserId)) { throw new BadRequestException('Invalid target user id'); } - if (currentUserId === targetUserId) { throw new BadRequestException('You cannot follow yourself'); } - const targetUser = await this.usersRepository.findById(targetUserId); - if (!targetUser) { - throw new NotFoundException('Target user not found'); - } + const targetUser = await this.findActiveTargetUser(targetUserId); const existing = await this.followsRepository.findOne(currentUserId, targetUserId); @@ -46,37 +43,107 @@ export class FollowsService { return { following: false }; } + const block = await this.blocksRepository.findAnyBetween(currentUserId, targetUserId); + if (block) { + throw new BadRequestException('You cannot follow this user'); + } + if (targetUser.isPrivate) { const request = await this.followsRepository.upsertPendingRequest(currentUserId, targetUserId); return { following: false, requested: true, requestId: request.id }; } - const follow = await this.followsRepository.create(currentUserId, targetUserId); + let followId: string | null = null; + try { + const follow = await this.followsRepository.create(currentUserId, targetUserId); + followId = follow.id; + } catch (error) { + if (!this.followsRepository.isDuplicateKeyError(error)) { + throw error; + } + } await this.syncFollowCounts(currentUserId, targetUserId); await this.feedVersionService.bumpGlobalVersion(); - try { - await this.outboxService.enqueueFollowNotification(currentUserId, targetUserId, follow.id); - } catch (error) { - this.logger.warn( - `Follow notification failed for actor=${currentUserId} recipient=${targetUserId}: ${ - error instanceof Error ? error.message : 'unknown error' - }`, - ); + if (followId) { + await this.enqueueFollowNotification(currentUserId, targetUserId, followId); } return { following: true }; } - async getFollowers(userId: string, query: PaginationQueryDto) { + async followUser(currentUserId: string, targetUserId: string) { + await this.assertCanFollowTarget(currentUserId, targetUserId); + const targetUser = await this.findActiveTargetUser(targetUserId); + + const existing = await this.followsRepository.findOne(currentUserId, targetUserId); + if (existing) { + const counts = await this.syncFollowCounts(currentUserId, targetUserId); + return this.buildFollowActionResponse('Already following user', true, targetUserId, counts); + } + + if (targetUser.isPrivate) { + const request = await this.followsRepository.upsertPendingRequest(currentUserId, targetUserId); + const counts = await this.getFollowCounts(currentUserId, targetUserId); + return { + ...this.buildFollowActionResponse('Follow request sent', false, targetUserId, counts), + requested: true, + requestId: request.id, + }; + } + + let followId: string | null = null; + try { + const follow = await this.followsRepository.create(currentUserId, targetUserId); + followId = follow.id; + } catch (error) { + if (!this.followsRepository.isDuplicateKeyError(error)) { + throw error; + } + } + + const counts = await this.syncFollowCounts(currentUserId, targetUserId); + await this.feedVersionService.bumpGlobalVersion(); + + if (followId) { + await this.enqueueFollowNotification(currentUserId, targetUserId, followId); + } + + return this.buildFollowActionResponse('User followed successfully', true, targetUserId, counts); + } + + async unfollowUser(currentUserId: string, targetUserId: string) { + if (!Types.ObjectId.isValid(targetUserId)) { + throw new BadRequestException('Invalid target user id'); + } + if (currentUserId === targetUserId) { + throw new BadRequestException('You cannot follow yourself'); + } + + const existing = await this.followsRepository.findOne(currentUserId, targetUserId); + if (existing) { + await this.followsRepository.deleteById(existing.id); + await this.feedVersionService.bumpGlobalVersion(); + } + + const counts = await this.syncFollowCounts(currentUserId, targetUserId); + return this.buildFollowActionResponse('User unfollowed successfully', false, targetUserId, counts); + } + + async getFollowers(userId: string, query: PaginationQueryDto, viewerUserId?: string) { + await this.assertListTargetExists(userId); const page = query.page ?? 1; const limit = query.limit ?? 20; const skip = (page - 1) * limit; const sort = { createdAt: resolveMongoSortDirection(query.sortOrder) } as Record; - const [items, total] = await Promise.all([ - this.followsRepository.findMany({ followingId: userId }, skip, limit, sort), + const [followRows, total] = await Promise.all([ + this.followsRepository.findFollowerUserIds(userId, skip, limit, sort), this.followsRepository.count({ followingId: userId }), ]); + const items = await this.decorateFollowUsers( + followRows.map((row) => row.followerId.toString()), + viewerUserId, + ); return buildPaginatedResponse(items, { page, @@ -86,15 +153,20 @@ export class FollowsService { }); } - async getFollowing(userId: string, query: PaginationQueryDto) { + async getFollowing(userId: string, query: PaginationQueryDto, viewerUserId?: string) { + await this.assertListTargetExists(userId); const page = query.page ?? 1; const limit = query.limit ?? 20; const skip = (page - 1) * limit; const sort = { createdAt: resolveMongoSortDirection(query.sortOrder) } as Record; - const [items, total] = await Promise.all([ - this.followsRepository.findMany({ followerId: userId }, skip, limit, sort), + const [followRows, total] = await Promise.all([ + this.followsRepository.findFollowingUserIds(userId, skip, limit, sort), this.followsRepository.count({ followerId: userId }), ]); + const items = await this.decorateFollowUsers( + followRows.map((row) => row.followingId.toString()), + viewerUserId, + ); return buildPaginatedResponse(items, { page, @@ -110,16 +182,28 @@ export class FollowsService { } if (currentUserId === targetUserId) { - return { following: false, targetUserId }; + return { + following: false, + isFollowing: false, + isFollowedBy: false, + isMutual: false, + targetUserId, + }; } - const existing = await this.followsRepository.findOne(currentUserId, targetUserId); + const [existing, reverseExisting] = await Promise.all([ + this.followsRepository.findOne(currentUserId, targetUserId), + this.followsRepository.findOne(targetUserId, currentUserId), + ]); const pendingRequest = currentUserId === targetUserId ? null : await this.followsRepository.findPendingRequest(currentUserId, targetUserId); return { following: !!existing, + isFollowing: !!existing, + isFollowedBy: !!reverseExisting, + isMutual: !!existing && !!reverseExisting, requested: !!pendingRequest, targetUserId, }; @@ -282,7 +366,10 @@ export class FollowsService { return count; } - private async syncFollowCounts(currentUserId: string, targetUserId: string): Promise { + private async syncFollowCounts( + currentUserId: string, + targetUserId: string, + ): Promise<{ followingCount: number; followersCount: number }> { const [followingCount, followersCount] = await Promise.all([ this.followsRepository.count({ followerId: currentUserId }), this.followsRepository.count({ followingId: targetUserId }), @@ -292,5 +379,102 @@ export class FollowsService { this.usersRepository.setFollowingCount(currentUserId, followingCount), this.usersRepository.setFollowersCount(targetUserId, followersCount), ]); + + return { followingCount, followersCount }; + } + + private async getFollowCounts( + currentUserId: string, + targetUserId: string, + ): Promise<{ followingCount: number; followersCount: number }> { + const [followingCount, followersCount] = await Promise.all([ + this.followsRepository.count({ followerId: currentUserId }), + this.followsRepository.count({ followingId: targetUserId }), + ]); + + return { followingCount, followersCount }; + } + + private async assertCanFollowTarget(currentUserId: string, targetUserId: string): Promise { + if (!Types.ObjectId.isValid(targetUserId)) { + throw new BadRequestException('Invalid target user id'); + } + + if (currentUserId === targetUserId) { + throw new BadRequestException('You cannot follow yourself'); + } + + const block = await this.blocksRepository.findAnyBetween(currentUserId, targetUserId); + if (block) { + throw new BadRequestException('You cannot follow this user'); + } + } + + private async findActiveTargetUser(targetUserId: string): Promise { + const targetUser = await this.usersRepository.findById(targetUserId); + if (!targetUser || targetUser.isDisabled) { + throw new NotFoundException('Target user not found'); + } + return targetUser; + } + + private async assertListTargetExists(userId: string): Promise { + if (!Types.ObjectId.isValid(userId)) { + throw new BadRequestException('Invalid user id'); + } + await this.findActiveTargetUser(userId); + } + + private async enqueueFollowNotification(actorId: string, recipientId: string, followId: string): Promise { + try { + await this.outboxService.enqueueFollowNotification(actorId, recipientId, followId); + } catch (error) { + this.logger.warn( + `Follow notification failed for actor=${actorId} recipient=${recipientId}: ${ + error instanceof Error ? error.message : 'unknown error' + }`, + ); + } + } + + private buildFollowActionResponse( + message: string, + isFollowing: boolean, + targetUserId: string, + counts: { followingCount: number; followersCount: number }, + ) { + return { + message, + following: isFollowing, + isFollowing, + targetUserId, + followersCount: counts.followersCount, + followingCount: counts.followingCount, + }; + } + + private async decorateFollowUsers(userIds: string[], viewerUserId?: string) { + const users = await this.usersRepository.findManyByIds(userIds); + const usersById = new Map(users.map((user) => [user.id, user])); + const viewerFollowingIds = viewerUserId ? new Set(await this.followsRepository.findFollowingIds(viewerUserId)) : new Set(); + + return userIds + .map((id) => usersById.get(id)) + .filter((user): user is UserDocument => !!user) + .map((user) => { + const object = user.toObject(); + return { + _id: object._id, + name: object.name, + stageName: object.stageName, + username: object.username, + avatar: object.avatar, + isVerified: object.isVerified, + isDisabled: object.isDisabled, + followersCount: object.followersCount ?? 0, + followingCount: object.followingCount ?? 0, + isFollowing: viewerFollowingIds.has(user.id), + }; + }); } } diff --git a/src/modules/follows/schemas/follow.schema.ts b/src/modules/follows/schemas/follow.schema.ts index cdbe9ef..c7d92ec 100644 --- a/src/modules/follows/schemas/follow.schema.ts +++ b/src/modules/follows/schemas/follow.schema.ts @@ -15,3 +15,5 @@ export class Follow { export const FollowSchema = SchemaFactory.createForClass(Follow); FollowSchema.index({ followerId: 1, followingId: 1 }, { unique: true }); +FollowSchema.index({ followerId: 1, createdAt: -1 }); +FollowSchema.index({ followingId: 1, createdAt: -1 }); diff --git a/src/modules/notifications/notifications.repository.ts b/src/modules/notifications/notifications.repository.ts index d5ae3ff..279a453 100644 --- a/src/modules/notifications/notifications.repository.ts +++ b/src/modules/notifications/notifications.repository.ts @@ -92,22 +92,28 @@ export class NotificationsRepository { } async markRead(recipientId: string, notificationId: string): Promise { - const updated = await this.notificationModel - .findOneAndUpdate( - { - _id: new Types.ObjectId(notificationId), - recipientId: new Types.ObjectId(recipientId), - }, - { - read: true, - readAt: new Date(), - }, - { new: true }, - ) - .populate({ path: 'actorId', select: 'name username stageName avatar isVerified isDisabled' }) + const notification = await this.notificationModel + .findOne({ + _id: new Types.ObjectId(notificationId), + recipientId: new Types.ObjectId(recipientId), + }) .exec(); - return updated; + if (!notification) { + return null; + } + + if (notification.read) { + await notification.populate({ path: 'actorId', select: 'name username stageName avatar isVerified isDisabled' }); + return notification; + } + + notification.read = true; + notification.readAt = notification.readAt ?? new Date(); + await notification.save(); + await notification.populate({ path: 'actorId', select: 'name username stageName avatar isVerified isDisabled' }); + + return notification; } async markAllRead(recipientId: string): Promise { diff --git a/src/modules/notifications/notifications.service.spec.ts b/src/modules/notifications/notifications.service.spec.ts index 8bf4268..a0d68d5 100644 --- a/src/modules/notifications/notifications.service.spec.ts +++ b/src/modules/notifications/notifications.service.spec.ts @@ -47,6 +47,7 @@ describe('NotificationsService', () => { await expect(service.markAllRead('user-1')).resolves.toEqual({ message: 'All notifications marked as read', + modifiedCount: 4, updatedCount: 4, unreadCount: 2, }); @@ -70,4 +71,68 @@ describe('NotificationsService', () => { await expect(service.markRead('user-1', 'invalid-id')).rejects.toBeInstanceOf(NotFoundException); expect(notificationsRepository.markRead).not.toHaveBeenCalled(); }); + + it('returns total unread count for the current user in the notifications list', async () => { + const notificationsRepository = { + findMine: jest.fn().mockResolvedValue([{ id: 'notification-1' }]), + countMine: jest.fn().mockResolvedValue(30), + countUnread: jest.fn().mockResolvedValue(7), + }; + const notificationsGateway = { + emitUnreadCount: jest.fn(), + }; + + const service = new NotificationsService( + notificationsRepository as any, + notificationsGateway as any, + ); + + await expect( + service.getMine('507f1f77bcf86cd799439011', { + page: 1, + limit: 20, + sortOrder: 'desc' as any, + }), + ).resolves.toMatchObject({ + count: 1, + page: 1, + limit: 20, + total: 30, + unreadCount: 7, + pagination: { + mode: 'offset', + hasNextPage: true, + }, + }); + expect(notificationsRepository.countUnread).toHaveBeenCalledWith('507f1f77bcf86cd799439011'); + }); + + it('creates system notifications with system resource mapping when requested', async () => { + const notificationsRepository = { + create: jest.fn().mockResolvedValue({ toJSON: () => ({ _id: 'notification-1' }) }), + countUnread: jest.fn().mockResolvedValue(1), + }; + const notificationsGateway = { + emitCreated: jest.fn(), + }; + + const service = new NotificationsService( + notificationsRepository as any, + notificationsGateway as any, + ); + + await service.create({ + actorId: '507f1f77bcf86cd799439011', + recipientId: '507f191e810c19729de860ea', + type: 'system', + }); + + expect(notificationsRepository.create).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'system', + resourceType: 'system', + deepLink: '', + }), + ); + }); }); diff --git a/src/modules/notifications/notifications.service.ts b/src/modules/notifications/notifications.service.ts index 461444d..6baa7ce 100644 --- a/src/modules/notifications/notifications.service.ts +++ b/src/modules/notifications/notifications.service.ts @@ -206,10 +206,13 @@ export class NotificationsService { const sort = { createdAt: resolveMongoSortDirection(query.sortOrder) } as Record; + const unreadFilter = { ...filter }; + delete unreadFilter.read; + const [items, total, unreadCount] = await Promise.all([ this.notificationsRepository.findMany(filter, skip, limit, sort), this.notificationsRepository.count(filter), - this.notificationsRepository.countUnreadAll(filter), + this.notificationsRepository.countUnreadAll(unreadFilter), ]); return { @@ -250,13 +253,14 @@ export class NotificationsService { } async markAllRead(recipientId: string) { - const updatedCount = await this.notificationsRepository.markAllRead(recipientId); + const modifiedCount = await this.notificationsRepository.markAllRead(recipientId); const unreadCount = await this.notificationsRepository.countUnread(recipientId); this.notificationsGateway.emitUnreadCount(recipientId, unreadCount); return { message: 'All notifications marked as read', - updatedCount, + modifiedCount, + updatedCount: modifiedCount, unreadCount, }; } @@ -277,6 +281,12 @@ export class NotificationsService { return 'Post shared'; case 'mention': return 'New mention'; + case 'reply': + return 'New reply'; + case 'system': + return 'Notification'; + case 'collaboration_request': + return 'Collaboration request'; default: return 'Notification'; } @@ -288,6 +298,10 @@ export class NotificationsService { return 'user'; case 'message': return 'conversation'; + case 'system': + return 'system'; + case 'collaboration_request': + return 'post'; default: return 'post'; } @@ -306,8 +320,8 @@ export class NotificationsService { return `/users/${referenceId}`; } - if (resourceType === 'comment' && referenceId) { - return `/comments/${referenceId}`; + if (resourceType === 'system') { + return ''; } if (referenceId) { diff --git a/src/modules/notifications/schemas/notification.schema.ts b/src/modules/notifications/schemas/notification.schema.ts index fa6b985..f0205b5 100644 --- a/src/modules/notifications/schemas/notification.schema.ts +++ b/src/modules/notifications/schemas/notification.schema.ts @@ -4,7 +4,18 @@ import { User } from '../../users/schemas/user.schema'; export type NotificationDocument = HydratedDocument; -export const NOTIFICATION_TYPES = ['like', 'comment', 'follow', 'message', 'save', 'share', 'mention'] as const; +export const NOTIFICATION_TYPES = [ + 'like', + 'comment', + 'follow', + 'message', + 'save', + 'share', + 'mention', + 'reply', + 'system', + 'collaboration_request', +] as const; export type NotificationType = (typeof NOTIFICATION_TYPES)[number]; @Schema({ timestamps: true, versionKey: false }) @@ -47,4 +58,6 @@ export const NotificationSchema = SchemaFactory.createForClass(Notification); NotificationSchema.index({ recipientId: 1, createdAt: -1 }); NotificationSchema.index({ recipientId: 1, read: 1, createdAt: -1 }); NotificationSchema.index({ recipientId: 1, read: 1, type: 1, createdAt: -1 }); +NotificationSchema.index({ recipientId: 1, type: 1, createdAt: -1 }); NotificationSchema.index({ recipientId: 1, resourceType: 1, createdAt: -1 }); +NotificationSchema.index({ referenceId: 1 }); diff --git a/src/modules/posts/posts-users.controller.ts b/src/modules/posts/posts-users.controller.ts new file mode 100644 index 0000000..a8f4a6c --- /dev/null +++ b/src/modules/posts/posts-users.controller.ts @@ -0,0 +1,24 @@ +import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { JwtPayload } from '../../common/interfaces/jwt-payload.interface'; +import { PostQueryDto } from './dto/post-query.dto'; +import { PostsService } from './posts.service'; + +@ApiTags('Users') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) +@Controller('users') +export class PostsUsersController { + constructor(private readonly postsService: PostsService) {} + + @Get(':userId/posts') + async findUserPosts( + @CurrentUser() user: JwtPayload, + @Param('userId') userId: string, + @Query() query: PostQueryDto, + ) { + return this.postsService.findUserPosts(userId, query, user.sub); + } +} diff --git a/src/modules/posts/posts.controller.ts b/src/modules/posts/posts.controller.ts index b9bb22d..98e195d 100644 --- a/src/modules/posts/posts.controller.ts +++ b/src/modules/posts/posts.controller.ts @@ -106,8 +106,12 @@ export class PostsController { @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('user/:userId') - async findUserPosts(@Param('userId') userId: string, @Query() query: PostQueryDto) { - return this.postsService.findUserPosts(userId, query); + async findUserPosts( + @CurrentUser() user: JwtPayload, + @Param('userId') userId: string, + @Query() query: PostQueryDto, + ) { + return this.postsService.findUserPosts(userId, query, user.sub); } @ApiBearerAuth() diff --git a/src/modules/posts/posts.module.ts b/src/modules/posts/posts.module.ts index a4415a0..834de91 100644 --- a/src/modules/posts/posts.module.ts +++ b/src/modules/posts/posts.module.ts @@ -5,6 +5,7 @@ import { NotificationsModule } from '../notifications/notifications.module'; import { UsersModule } from '../users/users.module'; import { Post, PostSchema } from './schemas/post.schema'; import { PostsController } from './posts.controller'; +import { PostsUsersController } from './posts-users.controller'; import { PostsRepository } from './posts.repository'; import { PostsService } from './posts.service'; @@ -20,7 +21,7 @@ import { PostsService } from './posts.service'; NotificationsModule, UsersModule, ], - controllers: [PostsController], + controllers: [PostsController, PostsUsersController], providers: [PostsService, PostsRepository], exports: [PostsService, PostsRepository], }) diff --git a/src/modules/posts/posts.service.ts b/src/modules/posts/posts.service.ts index 79d3a0a..2c75f94 100644 --- a/src/modules/posts/posts.service.ts +++ b/src/modules/posts/posts.service.ts @@ -1,6 +1,7 @@ import { BadRequestException, ForbiddenException, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { extname } from 'path'; -import { Types } from 'mongoose'; +import { Connection, Types } from 'mongoose'; +import { InjectConnection } from '@nestjs/mongoose'; import { ModerationStatus } from '../../common/enums/moderation-status.enum'; import { PostType } from '../../common/enums/post-type.enum'; import { PostVisibility } from '../../common/enums/post-visibility.enum'; @@ -72,6 +73,7 @@ export class PostsService { private readonly logger = new Logger(PostsService.name); constructor( + @InjectConnection() private readonly connection: Connection, private readonly postsRepository: PostsRepository, private readonly usersRepository: UsersRepository, private readonly storageService: ManagedStorageService, @@ -525,7 +527,7 @@ export class PostsService { return post; } - async findUserPosts(userId: string, query: PostQueryDto) { + async findUserPosts(userId: string, query: PostQueryDto, viewerUserId?: string) { if (!Types.ObjectId.isValid(userId)) { throw new BadRequestException('Invalid user id'); } @@ -538,6 +540,21 @@ export class PostsService { authorId: new Types.ObjectId(userId), isArchived: { $ne: true }, }; + if (viewerUserId && viewerUserId !== userId) { + const isBlocked = await this.hasBlockBetween(viewerUserId, userId); + if (isBlocked) { + return buildPaginatedResponse([], { + page, + limit, + total: 0, + offset: skip, + }); + } + const isFollowing = await this.isFollowing(viewerUserId, userId); + filter.visibility = isFollowing + ? { $in: [PostVisibility.PUBLIC, PostVisibility.FOLLOWERS] } + : PostVisibility.PUBLIC; + } if (query.visibility) { filter.visibility = query.visibility; } @@ -1602,4 +1619,30 @@ export class PostsService { return ''; } + + private async isFollowing(followerId: string, followingId: string): Promise { + if (!Types.ObjectId.isValid(followerId) || !Types.ObjectId.isValid(followingId)) { + return false; + } + const count = await this.connection.collection('follows').countDocuments({ + followerId: new Types.ObjectId(followerId), + followingId: new Types.ObjectId(followingId), + }); + return count > 0; + } + + private async hasBlockBetween(userA: string, userB: string): Promise { + if (!Types.ObjectId.isValid(userA) || !Types.ObjectId.isValid(userB)) { + return false; + } + const userAId = new Types.ObjectId(userA); + const userBId = new Types.ObjectId(userB); + const count = await this.connection.collection('blocks').countDocuments({ + $or: [ + { blockerId: userAId, blockedId: userBId }, + { blockerId: userBId, blockedId: userAId }, + ], + }); + return count > 0; + } } diff --git a/src/modules/users/schemas/user.schema.ts b/src/modules/users/schemas/user.schema.ts index c543b78..9394d31 100644 --- a/src/modules/users/schemas/user.schema.ts +++ b/src/modules/users/schemas/user.schema.ts @@ -99,6 +99,12 @@ export class User { @Prop({ default: false }) isEmailVerified!: boolean; + @Prop({ default: false, index: true }) + isOnline!: boolean; + + @Prop({ type: Date, default: null }) + lastSeenAt?: Date | null; + @Prop({ default: '', trim: true, maxlength: 120 }) shopName!: string; diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index de220a3..0d64103 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -288,11 +288,18 @@ export class UsersController { return this.usersService.getProfileOverview(id, user.sub); } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':id/presence') + async getPresence(@Param('id') id: string) { + return this.usersService.getPresence(id); + } + @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get(':id') - async findOne(@Param('id') id: string) { - return this.usersService.findPublicByIdOrFail(id); + async findOne(@CurrentUser() user: JwtPayload, @Param('id') id: string) { + return this.usersService.findPublicByIdForViewer(id, user.sub); } @ApiBearerAuth() diff --git a/src/modules/users/users.repository.ts b/src/modules/users/users.repository.ts index ce7ab32..7b14545 100644 --- a/src/modules/users/users.repository.ts +++ b/src/modules/users/users.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { ClientSession, FilterQuery, Model, UpdateQuery } from 'mongoose'; +import { ClientSession, FilterQuery, Model, Types, UpdateQuery } from 'mongoose'; import { User, UserDocument } from './schemas/user.schema'; @Injectable() @@ -69,6 +69,12 @@ export class UsersRepository { await this.userModel.findByIdAndUpdate(userId, { followingCount }, { new: false }).exec(); } + async setPresence(userId: string, isOnline: boolean, lastSeenAt: Date): Promise { + await this.userModel + .findByIdAndUpdate(userId, { isOnline, lastSeenAt }, { new: false }) + .exec(); + } + async findMany( filter: FilterQuery, skip: number, @@ -78,6 +84,16 @@ export class UsersRepository { return this.userModel.find(filter).sort(sort).skip(skip).limit(limit).exec(); } + async findManyByIds(ids: string[]): Promise { + if (!ids.length) { + return []; + } + + return this.userModel + .find({ _id: { $in: ids.map((id) => new Types.ObjectId(id)) } }) + .exec(); + } + async findByUsernames(usernames: string[]): Promise { if (!usernames.length) { return []; diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index 7eb61aa..46864ae 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -237,6 +237,40 @@ export class UsersService { return user; } + async findPublicByIdForViewer(userId: string, viewerUserId: string) { + const user = await this.findPublicByIdOrFail(userId); + const isFollowing = + viewerUserId === userId + ? false + : (await this.connection.collection('follows').countDocuments({ + followerId: new Types.ObjectId(viewerUserId), + followingId: new Types.ObjectId(userId), + })) > 0; + + return { + ...user.toObject(), + isFollowing, + following: isFollowing, + isOwnProfile: viewerUserId === userId, + }; + } + + async getPresence(userId: string) { + const user = await this.findPublicByIdOrFail(userId); + return { + userId: user.id, + isOnline: user.isOnline ?? false, + lastSeenAt: user.lastSeenAt ?? null, + }; + } + + async setPresence(userId: string, isOnline: boolean): Promise { + if (!Types.ObjectId.isValid(userId)) { + return; + } + await this.usersRepository.setPresence(userId, isOnline, new Date()); + } + async findByEmailWithPassword(email: string): Promise { return this.usersRepository.findOneWithPassword({ email: email.toLowerCase() }); } @@ -737,6 +771,7 @@ export class UsersService { viewerState: { isOwnProfile: viewerUserId === userId, following: followingState, + isFollowing: followingState, canMessage: viewerUserId !== userId, }, }; @@ -749,6 +784,7 @@ export class UsersService { viewerState: { isOwnProfile: false, following: false, + isFollowing: false, canMessage: false, }, };