From eedc50c2981386f04a5c9d365890f110dd96888f Mon Sep 17 00:00:00 2001 From: Jeri Nurmi Date: Mon, 15 Dec 2025 12:49:21 +0200 Subject: [PATCH 01/18] update release tag --- src/common/swagger/tags/tags.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/swagger/tags/tags.ts b/src/common/swagger/tags/tags.ts index fca201ec9..01346516a 100644 --- a/src/common/swagger/tags/tags.ts +++ b/src/common/swagger/tags/tags.ts @@ -4,7 +4,7 @@ import { ExternalDocumentationObject } from '@nestjs/swagger/dist/interfaces/ope * Swagger tag name */ export type SwaggerTagName = - | 'Release on 15.12.2025' + | 'Release on 29.12.2025' | 'Profile' | 'Auth' | 'Player' @@ -36,9 +36,9 @@ export type SwaggerTagName = * */ export const swaggerTags: Record = { - 'Release on 15.12.2025': { - name: 'Release on 15.12.2025', - description: 'Changes made on release 15.12.2025', + 'Release on 29.12.2025': { + name: 'Release on 29.12.2025', + description: 'Changes made on release 29.12.2025', }, Profile: { name: 'Profile', From 7f47797ccf1a454579f35b02acb2de045d33a301 Mon Sep 17 00:00:00 2001 From: hoan301298 Date: Mon, 15 Dec 2025 16:36:08 +0200 Subject: [PATCH 02/18] Create Friendship test module and builders/factory --- .../data/builder/FriendRequestDtoBuilder.ts | 32 ++++++++++++++ .../data/builder/FriendlistDtoBuilder.ts | 44 +++++++++++++++++++ .../data/builder/FriendshipBuilder.ts | 40 +++++++++++++++++ .../data/friendshipBuilderFactory.ts | 32 ++++++++++++++ .../friendship/modules/friendship.module.ts | 24 ++++++++++ .../friendship/modules/friendshipCommon.ts | 33 ++++++++++++++ 6 files changed, 205 insertions(+) create mode 100644 src/__tests__/friendship/data/builder/FriendRequestDtoBuilder.ts create mode 100644 src/__tests__/friendship/data/builder/FriendlistDtoBuilder.ts create mode 100644 src/__tests__/friendship/data/builder/FriendshipBuilder.ts create mode 100644 src/__tests__/friendship/data/friendshipBuilderFactory.ts create mode 100644 src/__tests__/friendship/modules/friendship.module.ts create mode 100644 src/__tests__/friendship/modules/friendshipCommon.ts diff --git a/src/__tests__/friendship/data/builder/FriendRequestDtoBuilder.ts b/src/__tests__/friendship/data/builder/FriendRequestDtoBuilder.ts new file mode 100644 index 000000000..5395e9294 --- /dev/null +++ b/src/__tests__/friendship/data/builder/FriendRequestDtoBuilder.ts @@ -0,0 +1,32 @@ +import { ObjectId } from "mongodb"; +import IDataBuilder from "src/__tests__/test_utils/interface/IDataBuilder"; +import { FriendRequestDto } from "src/friendship/dto/FriendRequest.dto"; + +export default class FriendRequestDtoBuilder + implements IDataBuilder +{ + private readonly base: FriendRequestDto = { + friendship_id: new ObjectId().toString(), + direction: 'incoming', // 'outgoing' + friend: undefined, + } + + build(): FriendRequestDto { + return {...this.base} as FriendRequestDto; + } + + setFriendship_id(friendship_id: string): this { + this.base.friendship_id = friendship_id; + return this + } + + setDirection(direction: string | undefined): this { + this.base.direction = direction; + return this; + } + + setFriend(friend: any): this { + this.base.friend = friend; + return this; + } +} \ No newline at end of file diff --git a/src/__tests__/friendship/data/builder/FriendlistDtoBuilder.ts b/src/__tests__/friendship/data/builder/FriendlistDtoBuilder.ts new file mode 100644 index 000000000..7ec1007e9 --- /dev/null +++ b/src/__tests__/friendship/data/builder/FriendlistDtoBuilder.ts @@ -0,0 +1,44 @@ +import { ObjectId } from "mongodb"; +import IDataBuilder from "src/__tests__/test_utils/interface/IDataBuilder"; +import { FriendlistDto } from "src/friendship/dto/friend-list.dto"; + +export default class FriendlistDtoBuilder + implements IDataBuilder +{ + private readonly base: FriendlistDto = { + _id: new ObjectId().toString(), + name: "defaultName", + avatar: undefined, + clanName: undefined, + clan_id: undefined, + } + + build(): FriendlistDto { + return {...this.base} as FriendlistDto; + } + + setId(Id: string): this { + this.base._id = Id; + return this; + } + + setName(name: string): this { + this.base.name = name; + return this; + } + + setAvatar(avatar: any | undefined): this { + this.base.avatar = avatar; + return this; + } + + setClanName(clanName: string | undefined): this { + this.base.clanName = clanName; + return this; + } + + setClanId(clanId: string | undefined): this { + this.base.clan_id = clanId; + return this; + } +} \ No newline at end of file diff --git a/src/__tests__/friendship/data/builder/FriendshipBuilder.ts b/src/__tests__/friendship/data/builder/FriendshipBuilder.ts new file mode 100644 index 000000000..dc31d8e41 --- /dev/null +++ b/src/__tests__/friendship/data/builder/FriendshipBuilder.ts @@ -0,0 +1,40 @@ +import { ObjectId } from "mongoose"; +import IDataBuilder from "src/__tests__/test_utils/interface/IDataBuilder"; +import { FriendshipStatus } from "src/friendship/enum/friendship-status.enum"; +import { Friendship } from "src/friendship/friendship.schema"; + +export default class FriendshipBuilder + implements IDataBuilder +{ + private readonly base: Friendship = { + playerA: undefined, + playerB: undefined, + status: FriendshipStatus.PENDING, + requester: undefined, + pairKey: undefined, + } + + build(): Friendship { + return {...this.base} as Friendship; + } + + setPlayerA(playerA: ObjectId | undefined): this { + this.base.playerA = playerA; + return this; + } + + setPlayerB(playerB: ObjectId | undefined): this { + this.base.playerB = playerB; + return this; + } + + setStatus(status: FriendshipStatus): this { + this.base.status = status; + return this; + } + + setRequester(requester: ObjectId | undefined): this { + this.base.requester = requester; + return this; + } +} \ No newline at end of file diff --git a/src/__tests__/friendship/data/friendshipBuilderFactory.ts b/src/__tests__/friendship/data/friendshipBuilderFactory.ts new file mode 100644 index 000000000..06d8496cc --- /dev/null +++ b/src/__tests__/friendship/data/friendshipBuilderFactory.ts @@ -0,0 +1,32 @@ +import FriendlistDtoBuilder from "./builder/FriendlistDtoBuilder"; +import FriendRequestDtoBuilder from "./builder/FriendRequestDtoBuilder"; +import FriendshipBuilder from "./builder/FriendshipBuilder"; + +type BuilderName = + | 'FriendlistDto' + | 'FriendRequestDto' + | 'Friendship'; + +type BuilderMap = { + FriendlistDto: FriendlistDtoBuilder; + FriendRequestDto: FriendRequestDtoBuilder; + Friendship: FriendshipBuilder; +}; + +export default class FriendshipBuilderFactory { + static getBuilder(builderName: T): BuilderMap[T] { + switch (builderName) { + case 'FriendlistDto': + return new FriendlistDtoBuilder() as BuilderMap[T]; + + case 'FriendRequestDto': + return new FriendRequestDtoBuilder() as BuilderMap[T]; + + case 'Friendship': + return new FriendshipBuilder() as BuilderMap[T]; + + default: + throw new Error(`Unknown builder name: ${builderName}`); + } + } +} \ No newline at end of file diff --git a/src/__tests__/friendship/modules/friendship.module.ts b/src/__tests__/friendship/modules/friendship.module.ts new file mode 100644 index 000000000..2c746bf53 --- /dev/null +++ b/src/__tests__/friendship/modules/friendship.module.ts @@ -0,0 +1,24 @@ +import mongoose from "mongoose"; +import { FriendshipService } from "src/friendship/friendship.service"; +import FriendshipCommonModule from "./friendshipCommon"; +import FriendshipNotifier from "src/friendship/friendship.notifier"; +import { ModelName } from "src/common/enum/modelName.enum"; +import { FriendshipSchema } from "src/friendship/friendship.schema"; + +export default class FriendshipModule { + private constructor() {} + + static async getFriendshipService() { + const module = await FriendshipCommonModule.getModule(); + return await module.resolve(FriendshipService); + } + + static async getFriendshipNotifier() { + const module = await FriendshipCommonModule.getModule(); + return await module.resolve(FriendshipNotifier); + } + + static getFriendshipModel() { + return mongoose.model(ModelName.FRIENDSHIP, FriendshipSchema); + } +} \ No newline at end of file diff --git a/src/__tests__/friendship/modules/friendshipCommon.ts b/src/__tests__/friendship/modules/friendshipCommon.ts new file mode 100644 index 000000000..72028fb1b --- /dev/null +++ b/src/__tests__/friendship/modules/friendshipCommon.ts @@ -0,0 +1,33 @@ +import { MongooseModule } from "@nestjs/mongoose"; +import { Test, TestingModule } from "@nestjs/testing"; +import { mongooseOptions, mongoString } from "src/__tests__/test_utils/const/db"; +import { ModelName } from "src/common/enum/modelName.enum"; +import FriendshipNotifier from "src/friendship/friendship.notifier"; +import { FriendshipSchema } from "src/friendship/friendship.schema"; +import { FriendshipService } from "src/friendship/friendship.service"; +import { PlayerModule } from "src/player/player.module"; +import { PlayerSchema } from "src/player/schemas/player.schema"; + +export default class FriendshipCommonModule { + private constructor() {} + + private static module: TestingModule; + + static async getModule() { + if (!FriendshipCommonModule.module) + FriendshipCommonModule.module = await Test.createTestingModule({ + imports: [ + MongooseModule.forRoot(mongoString, mongooseOptions), + MongooseModule.forFeature([ + { name: ModelName.FRIENDSHIP, schema: FriendshipSchema }, + { name: ModelName.PLAYER, schema: PlayerSchema }, + ]), + + PlayerModule + ], + providers: [FriendshipService], + }).compile(); + + return FriendshipCommonModule.module; + } +} \ No newline at end of file From bd2a1307fca4247a8895937a3e96253672e52a3f Mon Sep 17 00:00:00 2001 From: hoan301298 Date: Tue, 16 Dec 2025 02:30:03 +0200 Subject: [PATCH 03/18] fix import path, update typeof player_id for Friendship --- .../data/builder/FriendRequestDtoBuilder.ts | 4 ++-- .../data/builder/FriendlistDtoBuilder.ts | 4 ++-- .../friendship/data/builder/FriendshipBuilder.ts | 16 ++++++++-------- .../friendship/modules/friendship.module.ts | 8 ++++---- .../friendship/modules/friendshipCommon.ts | 16 ++++++++-------- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/__tests__/friendship/data/builder/FriendRequestDtoBuilder.ts b/src/__tests__/friendship/data/builder/FriendRequestDtoBuilder.ts index 5395e9294..15be39847 100644 --- a/src/__tests__/friendship/data/builder/FriendRequestDtoBuilder.ts +++ b/src/__tests__/friendship/data/builder/FriendRequestDtoBuilder.ts @@ -1,6 +1,6 @@ import { ObjectId } from "mongodb"; -import IDataBuilder from "src/__tests__/test_utils/interface/IDataBuilder"; -import { FriendRequestDto } from "src/friendship/dto/FriendRequest.dto"; +import IDataBuilder from "../../../test_utils/interface/IDataBuilder"; +import { FriendRequestDto } from "../../../../friendship/dto/FriendRequest.dto"; export default class FriendRequestDtoBuilder implements IDataBuilder diff --git a/src/__tests__/friendship/data/builder/FriendlistDtoBuilder.ts b/src/__tests__/friendship/data/builder/FriendlistDtoBuilder.ts index 7ec1007e9..f94a920a2 100644 --- a/src/__tests__/friendship/data/builder/FriendlistDtoBuilder.ts +++ b/src/__tests__/friendship/data/builder/FriendlistDtoBuilder.ts @@ -1,6 +1,6 @@ import { ObjectId } from "mongodb"; -import IDataBuilder from "src/__tests__/test_utils/interface/IDataBuilder"; -import { FriendlistDto } from "src/friendship/dto/friend-list.dto"; +import IDataBuilder from "../../../test_utils/interface/IDataBuilder"; +import { FriendlistDto } from "../../../../friendship/dto/friend-list.dto"; export default class FriendlistDtoBuilder implements IDataBuilder diff --git a/src/__tests__/friendship/data/builder/FriendshipBuilder.ts b/src/__tests__/friendship/data/builder/FriendshipBuilder.ts index dc31d8e41..fca736044 100644 --- a/src/__tests__/friendship/data/builder/FriendshipBuilder.ts +++ b/src/__tests__/friendship/data/builder/FriendshipBuilder.ts @@ -1,7 +1,7 @@ -import { ObjectId } from "mongoose"; -import IDataBuilder from "src/__tests__/test_utils/interface/IDataBuilder"; -import { FriendshipStatus } from "src/friendship/enum/friendship-status.enum"; -import { Friendship } from "src/friendship/friendship.schema"; +import { Types } from "mongoose"; +import IDataBuilder from "../../../test_utils/interface/IDataBuilder"; +import { FriendshipStatus } from "../../../../friendship/enum/friendship-status.enum"; +import { Friendship } from "../../../../friendship/friendship.schema"; export default class FriendshipBuilder implements IDataBuilder @@ -9,7 +9,7 @@ export default class FriendshipBuilder private readonly base: Friendship = { playerA: undefined, playerB: undefined, - status: FriendshipStatus.PENDING, + status: FriendshipStatus.ACCEPTED, requester: undefined, pairKey: undefined, } @@ -18,12 +18,12 @@ export default class FriendshipBuilder return {...this.base} as Friendship; } - setPlayerA(playerA: ObjectId | undefined): this { + setPlayerA(playerA: Types.ObjectId): this { this.base.playerA = playerA; return this; } - setPlayerB(playerB: ObjectId | undefined): this { + setPlayerB(playerB: Types.ObjectId): this { this.base.playerB = playerB; return this; } @@ -33,7 +33,7 @@ export default class FriendshipBuilder return this; } - setRequester(requester: ObjectId | undefined): this { + setRequester(requester: Types.ObjectId): this { this.base.requester = requester; return this; } diff --git a/src/__tests__/friendship/modules/friendship.module.ts b/src/__tests__/friendship/modules/friendship.module.ts index 2c746bf53..fc95f3ffc 100644 --- a/src/__tests__/friendship/modules/friendship.module.ts +++ b/src/__tests__/friendship/modules/friendship.module.ts @@ -1,9 +1,9 @@ import mongoose from "mongoose"; -import { FriendshipService } from "src/friendship/friendship.service"; +import { FriendshipService } from "../../../friendship/friendship.service"; import FriendshipCommonModule from "./friendshipCommon"; -import FriendshipNotifier from "src/friendship/friendship.notifier"; -import { ModelName } from "src/common/enum/modelName.enum"; -import { FriendshipSchema } from "src/friendship/friendship.schema"; +import FriendshipNotifier from "../../../friendship/friendship.notifier"; +import { ModelName } from "../../../common/enum/modelName.enum"; +import { FriendshipSchema } from "../../../friendship/friendship.schema"; export default class FriendshipModule { private constructor() {} diff --git a/src/__tests__/friendship/modules/friendshipCommon.ts b/src/__tests__/friendship/modules/friendshipCommon.ts index 72028fb1b..3121570dc 100644 --- a/src/__tests__/friendship/modules/friendshipCommon.ts +++ b/src/__tests__/friendship/modules/friendshipCommon.ts @@ -1,12 +1,12 @@ import { MongooseModule } from "@nestjs/mongoose"; import { Test, TestingModule } from "@nestjs/testing"; -import { mongooseOptions, mongoString } from "src/__tests__/test_utils/const/db"; -import { ModelName } from "src/common/enum/modelName.enum"; -import FriendshipNotifier from "src/friendship/friendship.notifier"; -import { FriendshipSchema } from "src/friendship/friendship.schema"; -import { FriendshipService } from "src/friendship/friendship.service"; -import { PlayerModule } from "src/player/player.module"; -import { PlayerSchema } from "src/player/schemas/player.schema"; +import { mongooseOptions, mongoString } from "../../test_utils/const/db"; +import { ModelName } from "../../../common/enum/modelName.enum"; +import FriendshipNotifier from "../../../friendship/friendship.notifier"; +import { FriendshipSchema } from "../../../friendship/friendship.schema"; +import { FriendshipService } from "../../../friendship/friendship.service"; +import { PlayerModule } from "../../../player/player.module"; +import { PlayerSchema } from "../../../player/schemas/player.schema"; export default class FriendshipCommonModule { private constructor() {} @@ -25,7 +25,7 @@ export default class FriendshipCommonModule { PlayerModule ], - providers: [FriendshipService], + providers: [FriendshipService, FriendshipNotifier], }).compile(); return FriendshipCommonModule.module; From 0c5b534d4969828697a2a5b59feb663d17540725 Mon Sep 17 00:00:00 2001 From: hoan301298 Date: Tue, 16 Dec 2025 02:35:16 +0200 Subject: [PATCH 04/18] update typeof player_id in friendship.schema --- src/friendship/friendship.schema.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/friendship/friendship.schema.ts b/src/friendship/friendship.schema.ts index 543a788fe..80590bb43 100644 --- a/src/friendship/friendship.schema.ts +++ b/src/friendship/friendship.schema.ts @@ -2,6 +2,7 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { HydratedDocument, Schema as MongooseSchema, + Types, UpdateQuery, } from 'mongoose'; import { ModelName } from '../common/enum/modelName.enum'; @@ -16,14 +17,14 @@ export class Friendship { ref: ModelName.PLAYER, required: true, }) - playerA: MongooseSchema.Types.ObjectId; + playerA: Types.ObjectId; @Prop({ type: MongooseSchema.Types.ObjectId, ref: ModelName.PLAYER, required: true, }) - playerB: MongooseSchema.Types.ObjectId; + playerB: Types.ObjectId; @Prop({ type: String, @@ -36,11 +37,11 @@ export class Friendship { @Prop({ type: MongooseSchema.Types.ObjectId, ref: ModelName.PLAYER, - required: function () { + required: function (this: Friendship) { return this.status === FriendshipStatus.PENDING; }, }) - requester?: MongooseSchema.Types.ObjectId; + requester?: Types.ObjectId; @Prop({ type: String, required: true }) pairKey: string; From d578315fdabf91a5830892ee6db3560558710d78 Mon Sep 17 00:00:00 2001 From: hoan301298 Date: Tue, 16 Dec 2025 02:37:36 +0200 Subject: [PATCH 05/18] return NotFoundServiceError instead of throw error --- src/friendship/friendship.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/friendship/friendship.service.ts b/src/friendship/friendship.service.ts index ec41d6352..fa7a63e0c 100644 --- a/src/friendship/friendship.service.ts +++ b/src/friendship/friendship.service.ts @@ -43,7 +43,7 @@ export class FriendshipService { playerId, FriendshipStatus.ACCEPTED, ); - if (error) throw error; + if (error) return [null, error]; const filtered: FriendlistDto[] = friendships.map((doc) => { if (!doc.playerA || !doc.playerB) return null; From 959db3117a441a2b70acdd37d7652101aac6e3c1 Mon Sep 17 00:00:00 2001 From: hoan301298 Date: Tue, 16 Dec 2025 02:41:34 +0200 Subject: [PATCH 06/18] add test for getPlayerFriendlist --- .../getPlayerFriendlist.test.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts diff --git a/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts b/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts new file mode 100644 index 000000000..8e9b8cf0f --- /dev/null +++ b/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts @@ -0,0 +1,80 @@ +import { FriendshipService } from '../../../friendship/friendship.service'; +import FriendshipModule from '../modules/friendship.module'; +import FriendshipBuilderFactory from '../data/friendshipBuilderFactory'; +import { Friendship } from '../../../friendship/friendship.schema'; +import { Types } from 'mongoose'; +import { FriendshipStatus } from '../../../friendship/enum/friendship-status.enum'; + +describe('Friendship.getPlayerFriendlist() test suite', () => { + let friendshipService: FriendshipService; + const friendshipModel = FriendshipModule.getFriendshipModel(); + const friendshipBuilder = FriendshipBuilderFactory.getBuilder('Friendship'); + + const player1_id = new Types.ObjectId(); + const player2_id = new Types.ObjectId(); + const player3_id = new Types.ObjectId(); + + const createdFriendships: Friendship[] = []; + + beforeAll(() => { + const friendshipToCreate1 = friendshipBuilder + .setPlayerA(player1_id) + .setPlayerB(player2_id) + .setStatus(FriendshipStatus.ACCEPTED) + .build(); + + createdFriendships.push(friendshipToCreate1); + + const friendshipToCreate2 = friendshipBuilder + .setPlayerA(player1_id) + .setPlayerB(player3_id) + .setStatus(FriendshipStatus.ACCEPTED) + .build(); + + createdFriendships.push(friendshipToCreate2); + + const friendshipToCreate3 = friendshipBuilder + .setPlayerA(player2_id) + .setPlayerB(player3_id) + .setStatus(FriendshipStatus.PENDING) + .setRequester(player2_id) + .build(); + + createdFriendships.push(friendshipToCreate3); + }); + + beforeEach(async () => { + await friendshipModel.deleteMany({}); + friendshipService = await FriendshipModule.getFriendshipService(); + + await friendshipModel.create(...createdFriendships); + }); + + it('Should get two friendships with status ACCEPTED', async () => { + const [friendships, err] = await friendshipService.getPlayerFriendlist( + player1_id.toString(), + ); + + expect(err).toBeNull(); + expect(friendships).toHaveLength(2); + }); + + it('Should return an empty array if no player_id match the filter', async () => { + const randomPlayer_id = new Types.ObjectId(); + const [friendships, err] = await friendshipService.getPlayerFriendlist( + randomPlayer_id.toString(), + ); + + expect(err).toContainSE_NOT_FOUND(); + expect(friendships).toBeNull(); + }); + + it('Should return only one friendship for player2', async () => { + const [friendships, err] = await friendshipService.getPlayerFriendlist( + player2_id.toString(), + ); + + expect(err).toBeNull(); + expect(friendships).toHaveLength(1); + }); +}); From d00f17ad4832e92a567498010d38088997b1c43b Mon Sep 17 00:00:00 2001 From: hoan301298 Date: Tue, 16 Dec 2025 23:27:53 +0200 Subject: [PATCH 07/18] add return type for getFriendRequests() and return NOT_FOUND error instead of throw --- src/friendship/friendship.service.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/friendship/friendship.service.ts b/src/friendship/friendship.service.ts index fa7a63e0c..d0f690f6d 100644 --- a/src/friendship/friendship.service.ts +++ b/src/friendship/friendship.service.ts @@ -14,6 +14,7 @@ import { FriendlistDto } from './dto/friend-list.dto'; import { Player } from '../player/schemas/player.schema'; import FriendshipNotifier from './friendship.notifier'; import { InvalidIdsServiceError } from './error/duplicateId.error'; +import { FriendRequestDto } from './dto/FriendRequest.dto'; @Injectable() export class FriendshipService { @@ -37,7 +38,7 @@ export class FriendshipService { */ async getPlayerFriendlist( playerId: string, - ): Promise> { + ): Promise> { try { const [friendships, error] = await this.getFriendshipsWithStatus( playerId, @@ -57,10 +58,10 @@ export class FriendshipService { avatar: friend.avatar, clanName: friend.Clan?.name ?? null, clan_id: friend.clan_id.toString(), - }; + } as FriendlistDto; }); - return [filtered as any, null]; + return [filtered, null]; } catch (error) { const errors = convertMongooseToServiceErrors(error); return [null, errors]; @@ -75,15 +76,17 @@ export class FriendshipService { * @param - playerId of the player whose friendlist to return * @returns Friend request list */ - async getFriendRequests(playerId: string) { + async getFriendRequests( + playerId: string + ): Promise> { try { const [requests, error] = await this.getFriendshipsWithStatus( playerId, FriendshipStatus.PENDING, ); - if (error) throw error; + if (error) return [null, error]; - const filtered = requests.map((doc) => { + const filtered: FriendRequestDto[] = requests.map((doc) => { if (!doc.playerA || !doc.playerB) return null; const friend = doc.playerA._id.toString() === playerId ? doc.playerB : doc.playerA; @@ -100,7 +103,7 @@ export class FriendshipService { clan_id: friend.clan_id.toString(), clanName: friend.Clan?.name ?? null, }, - }; + } as FriendRequestDto; }); return [filtered, null]; From ea1bd6934a6be2ad9b5e068d1197b68b1e8ae8d6 Mon Sep 17 00:00:00 2001 From: hoan301298 Date: Tue, 16 Dec 2025 23:30:41 +0200 Subject: [PATCH 08/18] validate requester if status is PENDING --- src/friendship/friendship.schema.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/friendship/friendship.schema.ts b/src/friendship/friendship.schema.ts index 80590bb43..5ca5ecadc 100644 --- a/src/friendship/friendship.schema.ts +++ b/src/friendship/friendship.schema.ts @@ -56,9 +56,21 @@ FriendshipSchema.pre('validate', function (next) { const b = this.playerB.toString(); if (a === b) return next(new Error('playerA and playerB cannot be the same')); + if (this.status === FriendshipStatus.PENDING) { + if (!this.requester) { + return next(new Error('requester is required when status is PENDING')); + } + + const requesterStr = this.requester.toString(); + if (requesterStr !== a && requesterStr !== b) { + return next(new Error('requester must be either playerA or playerB')); + } + } + this.pairKey = [a, b].sort().join('_'); next(); }); + FriendshipSchema.pre('save', function (next) { if (this.isModified('status') && this.status === FriendshipStatus.ACCEPTED) { this.requester = undefined; From 68ea6e54b4bdc343224145c70c79a73197cba434 Mon Sep 17 00:00:00 2001 From: hoan301298 Date: Tue, 16 Dec 2025 23:33:52 +0200 Subject: [PATCH 09/18] fix build() format of builder --- .../data/builder/FriendshipBuilder.ts | 75 ++++++++++--------- .../getPlayerFriendlist.test.ts | 6 +- 2 files changed, 44 insertions(+), 37 deletions(-) diff --git a/src/__tests__/friendship/data/builder/FriendshipBuilder.ts b/src/__tests__/friendship/data/builder/FriendshipBuilder.ts index fca736044..f5df6688f 100644 --- a/src/__tests__/friendship/data/builder/FriendshipBuilder.ts +++ b/src/__tests__/friendship/data/builder/FriendshipBuilder.ts @@ -1,40 +1,47 @@ -import { Types } from "mongoose"; -import IDataBuilder from "../../../test_utils/interface/IDataBuilder"; -import { FriendshipStatus } from "../../../../friendship/enum/friendship-status.enum"; -import { Friendship } from "../../../../friendship/friendship.schema"; - -export default class FriendshipBuilder - implements IDataBuilder -{ - private readonly base: Friendship = { - playerA: undefined, - playerB: undefined, - status: FriendshipStatus.ACCEPTED, - requester: undefined, - pairKey: undefined, - } +import { Types } from 'mongoose'; +import IDataBuilder from '../../../test_utils/interface/IDataBuilder'; +import { FriendshipStatus } from '../../../../friendship/enum/friendship-status.enum'; +import { Friendship } from '../../../../friendship/friendship.schema'; - build(): Friendship { - return {...this.base} as Friendship; - } +export default class FriendshipBuilder implements IDataBuilder { + private playerA: Types.ObjectId; + private playerB: Types.ObjectId; + private status: FriendshipStatus; + private requester?: Types.ObjectId; + private pairKey?: string; - setPlayerA(playerA: Types.ObjectId): this { - this.base.playerA = playerA; - return this; - } + build(): Friendship { + const friendship: any = { + playerA: this.playerA, + playerB: this.playerB, + status: this.status, + pairKey: this.pairKey, + }; - setPlayerB(playerB: Types.ObjectId): this { - this.base.playerB = playerB; - return this; + if (this.requester !== undefined) { + friendship.requester = this.requester; } - setStatus(status: FriendshipStatus): this { - this.base.status = status; - return this; - } + return friendship as Friendship; + } - setRequester(requester: Types.ObjectId): this { - this.base.requester = requester; - return this; - } -} \ No newline at end of file + setPlayerA(playerA: Types.ObjectId): this { + this.playerA = playerA; + return this; + } + + setPlayerB(playerB: Types.ObjectId): this { + this.playerB = playerB; + return this; + } + + setStatus(status: FriendshipStatus): this { + this.status = status; + return this; + } + + setRequester(requester: Types.ObjectId): this { + this.requester = requester; + return this; + } +} diff --git a/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts b/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts index 8e9b8cf0f..dfb881242 100644 --- a/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts +++ b/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts @@ -47,10 +47,10 @@ describe('Friendship.getPlayerFriendlist() test suite', () => { await friendshipModel.deleteMany({}); friendshipService = await FriendshipModule.getFriendshipService(); - await friendshipModel.create(...createdFriendships); + await friendshipModel.create(createdFriendships); }); - it('Should get two friendships with status ACCEPTED', async () => { + it('Should get two friendships for player1', async () => { const [friendships, err] = await friendshipService.getPlayerFriendlist( player1_id.toString(), ); @@ -69,7 +69,7 @@ describe('Friendship.getPlayerFriendlist() test suite', () => { expect(friendships).toBeNull(); }); - it('Should return only one friendship for player2', async () => { + it('Should get only one friendship for player2', async () => { const [friendships, err] = await friendshipService.getPlayerFriendlist( player2_id.toString(), ); From 0d2b0bab7e67b5468d5eca8ebb8d9d032940ced7 Mon Sep 17 00:00:00 2001 From: hoan301298 Date: Tue, 16 Dec 2025 23:34:45 +0200 Subject: [PATCH 10/18] add tests for friendRequest --- .../getFriendRequests.test.ts | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 src/__tests__/friendship/friendshipService/getFriendRequests.test.ts diff --git a/src/__tests__/friendship/friendshipService/getFriendRequests.test.ts b/src/__tests__/friendship/friendshipService/getFriendRequests.test.ts new file mode 100644 index 000000000..f4078b6e4 --- /dev/null +++ b/src/__tests__/friendship/friendshipService/getFriendRequests.test.ts @@ -0,0 +1,129 @@ +import { Types } from 'mongoose'; +import { FriendshipService } from '../../../friendship/friendship.service'; +import FriendshipBuilderFactory from '../data/friendshipBuilderFactory'; +import FriendshipModule from '../modules/friendship.module'; +import { Friendship } from '../../../friendship/friendship.schema'; +import { FriendshipStatus } from '../../../friendship/enum/friendship-status.enum'; + +describe('Friendship.getFriendRequests() test suites', () => { + let friendshipService: FriendshipService; + const friendshipModel = FriendshipModule.getFriendshipModel(); + + const player1_id = new Types.ObjectId(); + const player2_id = new Types.ObjectId(); + const player3_id = new Types.ObjectId(); + const player4_id = new Types.ObjectId(); + + const createdFriendships: Friendship[] = []; + + beforeAll(() => { + const friendshipConfigs = [ + { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player1_id, + playerB: player3_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player2_id, + playerB: player3_id, + status: FriendshipStatus.ACCEPTED, + }, + { + playerA: player1_id, + playerB: player4_id, + status: FriendshipStatus.BLOCKED, + }, + ]; + + for (const config of friendshipConfigs) { + const friendshipBuilder = + FriendshipBuilderFactory.getBuilder('Friendship'); + + friendshipBuilder + .setPlayerA(config.playerA) + .setPlayerB(config.playerB) + .setStatus(config.status); + + if (config.requester) { + friendshipBuilder.setRequester(config.requester); + } + + createdFriendships.push(friendshipBuilder.build()); + } + }); + + beforeEach(async () => { + await friendshipModel.deleteMany({}); + friendshipService = await FriendshipModule.getFriendshipService(); + + await friendshipModel.create(createdFriendships); + }); + + it('Should get two friendship requests for player1', async () => { + const [friendships, err] = await friendshipService.getFriendRequests( + player1_id.toString(), + ); + + expect(err).toBeNull(); + expect(friendships).toHaveLength(2); + }); + + it('Should get one friendship request for player2', async () => { + const [friendships, err] = await friendshipService.getFriendRequests( + player2_id.toString(), + ); + + expect(err).toBeNull(); + expect(friendships).toHaveLength(1); + }); + + it('should return NOT_FOUND for player4', async () => { + const [friendships, err] = await friendshipService.getFriendRequests( + player4_id.toString(), + ); + + expect(err).toContainSE_NOT_FOUND(); + expect(friendships).toBeNull(); + }); + + it('...', async () => { + const [friendships, err] = await friendshipService.getFriendRequests( + player3_id.toString(), + ); + + expect(err).toBeNull(); + expect(friendships).toHaveLength(1); + }); + + it('Direct create should fail without requester', async () => { + const invalidFriendship = { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.PENDING, + }; + + await expect(friendshipModel.create(invalidFriendship)).rejects.toThrow( + /requester is required when status is PENDING/, + ); + }); + + it('Direct create should fail with wrong requester', async () => { + const invalidFriendship = { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.PENDING, + requester: player3_id, + }; + + await expect(friendshipModel.create(invalidFriendship)).rejects.toThrow( + /requester must be either playerA or playerB/, + ); + }); +}); From 083093ca0fb37bdbe72ad2eb6ebbc644c741a6fb Mon Sep 17 00:00:00 2001 From: hoan301298 Date: Wed, 17 Dec 2025 10:59:12 +0200 Subject: [PATCH 11/18] correct type --- .../friendship/data/builder/FriendshipBuilder.ts | 13 +++++++------ .../friendshipService/getPlayerFriendlist.test.ts | 10 +++++----- src/friendship/friendship.schema.ts | 8 ++++---- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/__tests__/friendship/data/builder/FriendshipBuilder.ts b/src/__tests__/friendship/data/builder/FriendshipBuilder.ts index f5df6688f..6b94d13f1 100644 --- a/src/__tests__/friendship/data/builder/FriendshipBuilder.ts +++ b/src/__tests__/friendship/data/builder/FriendshipBuilder.ts @@ -2,12 +2,13 @@ import { Types } from 'mongoose'; import IDataBuilder from '../../../test_utils/interface/IDataBuilder'; import { FriendshipStatus } from '../../../../friendship/enum/friendship-status.enum'; import { Friendship } from '../../../../friendship/friendship.schema'; +import { ObjectId } from 'mongodb'; export default class FriendshipBuilder implements IDataBuilder { - private playerA: Types.ObjectId; - private playerB: Types.ObjectId; + private playerA: string | ObjectId; + private playerB: string | ObjectId; private status: FriendshipStatus; - private requester?: Types.ObjectId; + private requester?: string | ObjectId; private pairKey?: string; build(): Friendship { @@ -25,12 +26,12 @@ export default class FriendshipBuilder implements IDataBuilder { return friendship as Friendship; } - setPlayerA(playerA: Types.ObjectId): this { + setPlayerA(playerA: string | ObjectId): this { this.playerA = playerA; return this; } - setPlayerB(playerB: Types.ObjectId): this { + setPlayerB(playerB: string | ObjectId): this { this.playerB = playerB; return this; } @@ -40,7 +41,7 @@ export default class FriendshipBuilder implements IDataBuilder { return this; } - setRequester(requester: Types.ObjectId): this { + setRequester(requester: string | ObjectId): this { this.requester = requester; return this; } diff --git a/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts b/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts index dfb881242..422d2d737 100644 --- a/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts +++ b/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts @@ -2,17 +2,17 @@ import { FriendshipService } from '../../../friendship/friendship.service'; import FriendshipModule from '../modules/friendship.module'; import FriendshipBuilderFactory from '../data/friendshipBuilderFactory'; import { Friendship } from '../../../friendship/friendship.schema'; -import { Types } from 'mongoose'; import { FriendshipStatus } from '../../../friendship/enum/friendship-status.enum'; +import { ObjectId } from 'mongodb'; describe('Friendship.getPlayerFriendlist() test suite', () => { let friendshipService: FriendshipService; const friendshipModel = FriendshipModule.getFriendshipModel(); const friendshipBuilder = FriendshipBuilderFactory.getBuilder('Friendship'); - const player1_id = new Types.ObjectId(); - const player2_id = new Types.ObjectId(); - const player3_id = new Types.ObjectId(); + const player1_id = new ObjectId(); + const player2_id = new ObjectId(); + const player3_id = new ObjectId(); const createdFriendships: Friendship[] = []; @@ -60,7 +60,7 @@ describe('Friendship.getPlayerFriendlist() test suite', () => { }); it('Should return an empty array if no player_id match the filter', async () => { - const randomPlayer_id = new Types.ObjectId(); + const randomPlayer_id = new ObjectId(); const [friendships, err] = await friendshipService.getPlayerFriendlist( randomPlayer_id.toString(), ); diff --git a/src/friendship/friendship.schema.ts b/src/friendship/friendship.schema.ts index 5ca5ecadc..8f3672a0a 100644 --- a/src/friendship/friendship.schema.ts +++ b/src/friendship/friendship.schema.ts @@ -2,11 +2,11 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { HydratedDocument, Schema as MongooseSchema, - Types, UpdateQuery, } from 'mongoose'; import { ModelName } from '../common/enum/modelName.enum'; import { FriendshipStatus } from './enum/friendship-status.enum'; +import { ObjectId } from 'mongodb'; export type FriendshipDocument = HydratedDocument; @@ -17,14 +17,14 @@ export class Friendship { ref: ModelName.PLAYER, required: true, }) - playerA: Types.ObjectId; + playerA: string | ObjectId; @Prop({ type: MongooseSchema.Types.ObjectId, ref: ModelName.PLAYER, required: true, }) - playerB: Types.ObjectId; + playerB: string | ObjectId; @Prop({ type: String, @@ -41,7 +41,7 @@ export class Friendship { return this.status === FriendshipStatus.PENDING; }, }) - requester?: Types.ObjectId; + requester?: string | ObjectId; @Prop({ type: String, required: true }) pairKey: string; From 2d5580d007eb44d80bb95b3915231218e523eaad Mon Sep 17 00:00:00 2001 From: hoan301298 Date: Wed, 17 Dec 2025 11:04:38 +0200 Subject: [PATCH 12/18] remove unused --- src/__tests__/friendship/data/builder/FriendshipBuilder.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/__tests__/friendship/data/builder/FriendshipBuilder.ts b/src/__tests__/friendship/data/builder/FriendshipBuilder.ts index 6b94d13f1..9eb9c4849 100644 --- a/src/__tests__/friendship/data/builder/FriendshipBuilder.ts +++ b/src/__tests__/friendship/data/builder/FriendshipBuilder.ts @@ -1,4 +1,3 @@ -import { Types } from 'mongoose'; import IDataBuilder from '../../../test_utils/interface/IDataBuilder'; import { FriendshipStatus } from '../../../../friendship/enum/friendship-status.enum'; import { Friendship } from '../../../../friendship/friendship.schema'; From 19a7d1d484ac01e75bfa9fe7c48807257c390b94 Mon Sep 17 00:00:00 2001 From: hoan301298 Date: Wed, 17 Dec 2025 17:32:37 +0200 Subject: [PATCH 13/18] add clanModel --- .../friendship/modules/friendship.module.ts | 14 ++++++++++++-- .../friendship/modules/friendshipCommon.ts | 2 ++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/__tests__/friendship/modules/friendship.module.ts b/src/__tests__/friendship/modules/friendship.module.ts index fc95f3ffc..a66a753b7 100644 --- a/src/__tests__/friendship/modules/friendship.module.ts +++ b/src/__tests__/friendship/modules/friendship.module.ts @@ -4,6 +4,8 @@ import FriendshipCommonModule from "./friendshipCommon"; import FriendshipNotifier from "../../../friendship/friendship.notifier"; import { ModelName } from "../../../common/enum/modelName.enum"; import { FriendshipSchema } from "../../../friendship/friendship.schema"; +import { ClanSchema } from "../../../clan/clan.schema"; +import { PlayerSchema } from "../../../player/schemas/player.schema"; export default class FriendshipModule { private constructor() {} @@ -11,14 +13,22 @@ export default class FriendshipModule { static async getFriendshipService() { const module = await FriendshipCommonModule.getModule(); return await module.resolve(FriendshipService); - } + }; static async getFriendshipNotifier() { const module = await FriendshipCommonModule.getModule(); return await module.resolve(FriendshipNotifier); - } + }; static getFriendshipModel() { return mongoose.model(ModelName.FRIENDSHIP, FriendshipSchema); + }; + + static getClanModel() { + return mongoose.model(ModelName.CLAN, ClanSchema); + }; + + static getPlayerModel() { + return mongoose.model(ModelName.PLAYER, PlayerSchema); } } \ No newline at end of file diff --git a/src/__tests__/friendship/modules/friendshipCommon.ts b/src/__tests__/friendship/modules/friendshipCommon.ts index 3121570dc..165a9021a 100644 --- a/src/__tests__/friendship/modules/friendshipCommon.ts +++ b/src/__tests__/friendship/modules/friendshipCommon.ts @@ -7,6 +7,7 @@ import { FriendshipSchema } from "../../../friendship/friendship.schema"; import { FriendshipService } from "../../../friendship/friendship.service"; import { PlayerModule } from "../../../player/player.module"; import { PlayerSchema } from "../../../player/schemas/player.schema"; +import { ClanSchema } from "../../../clan/clan.schema"; export default class FriendshipCommonModule { private constructor() {} @@ -21,6 +22,7 @@ export default class FriendshipCommonModule { MongooseModule.forFeature([ { name: ModelName.FRIENDSHIP, schema: FriendshipSchema }, { name: ModelName.PLAYER, schema: PlayerSchema }, + { name: ModelName.CLAN, schema: ClanSchema }, ]), PlayerModule From ae903956c810c94ccada93dcdf725c172fa2c22e Mon Sep 17 00:00:00 2001 From: hoan301298 Date: Wed, 17 Dec 2025 17:40:59 +0200 Subject: [PATCH 14/18] add test for notifier and mock data function --- .../data/mockData/createData.mock.ts | 45 +++++++ .../getFriendRequests.test.ts | 2 +- .../sendNewFriendRequestNotification.test.ts | 111 ++++++++++++++++++ 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/friendship/data/mockData/createData.mock.ts create mode 100644 src/__tests__/friendship/friendshipService/sendNewFriendRequestNotification.test.ts diff --git a/src/__tests__/friendship/data/mockData/createData.mock.ts b/src/__tests__/friendship/data/mockData/createData.mock.ts new file mode 100644 index 000000000..42f1e1aed --- /dev/null +++ b/src/__tests__/friendship/data/mockData/createData.mock.ts @@ -0,0 +1,45 @@ +import { Player } from '../../../../player/schemas/player.schema'; +import FriendshipModule from '../../modules/friendship.module'; +import PlayerBuilder from '../../../player/data/player/playerBuilder'; +import { Clan } from '../../../../clan/clan.schema'; +import ClanBuilder from '../../../clan/data/clan/ClanBuilder'; + +export async function createMockPlayers( + overides: Partial[], +): Promise { + const model = FriendshipModule.getPlayerModel(); + const players: Player[] = []; + + for (const overide of overides) { + const builder = new PlayerBuilder() + .setId(overide._id) + .setName(overide.name) + .setUniqueIdentifier(overide.uniqueIdentifier) + .setClanId(overide.clan_id) + .build(); + + const savedPlayer = await model.create(builder); + players.push(savedPlayer); + } + + return players; +} + +export async function createMockClans( + overides: Partial[], +): Promise { + const model = FriendshipModule.getClanModel(); + const clans: Clan[] = []; + + for (const overide of overides) { + const builder = new ClanBuilder() + .setId(overide._id) + .setName(overide.name) + .build(); + + const savedClans = await model.create(builder); + clans.push(savedClans); + } + + return clans; +} diff --git a/src/__tests__/friendship/friendshipService/getFriendRequests.test.ts b/src/__tests__/friendship/friendshipService/getFriendRequests.test.ts index f4078b6e4..6cf672522 100644 --- a/src/__tests__/friendship/friendshipService/getFriendRequests.test.ts +++ b/src/__tests__/friendship/friendshipService/getFriendRequests.test.ts @@ -93,7 +93,7 @@ describe('Friendship.getFriendRequests() test suites', () => { expect(friendships).toBeNull(); }); - it('...', async () => { + it('Should avoid status ACCEPTED and return one friendship request', async () => { const [friendships, err] = await friendshipService.getFriendRequests( player3_id.toString(), ); diff --git a/src/__tests__/friendship/friendshipService/sendNewFriendRequestNotification.test.ts b/src/__tests__/friendship/friendshipService/sendNewFriendRequestNotification.test.ts new file mode 100644 index 000000000..fba1e0c97 --- /dev/null +++ b/src/__tests__/friendship/friendshipService/sendNewFriendRequestNotification.test.ts @@ -0,0 +1,111 @@ +import { ObjectId } from 'mongodb'; +import { FriendshipService } from '../../../friendship/friendship.service'; +import FriendshipModule from '../modules/friendship.module'; +import FriendshipBuilderFactory from '../data/friendshipBuilderFactory'; +import { FriendshipStatus } from '../../../friendship/enum/friendship-status.enum'; +import { Friendship } from '../../../friendship/friendship.schema'; +import { + createMockClans, + createMockPlayers, +} from '../data/mockData/createData.mock'; + +describe('FriendshipService.sendNewFriendRequestNotification()', () => { + let friendshipService: FriendshipService; + const clanModel = FriendshipModule.getClanModel(); + const playerModel = FriendshipModule.getPlayerModel(); + const friendshipModel = FriendshipModule.getFriendshipModel(); + const createdFriendships: Friendship[] = []; + + const player1_id = new ObjectId().toString(); + const player2_id = new ObjectId().toString(); + const player3_id = new ObjectId().toString(); + + const clan_id = new ObjectId().toString(); + + beforeAll(() => { + const friendshipConfigs = [ + { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player1_id, + playerB: player3_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player2_id, + playerB: player3_id, + status: FriendshipStatus.PENDING, + requester: player2_id, + }, + ]; + + for (const config of friendshipConfigs) { + const builder = FriendshipBuilderFactory.getBuilder('Friendship'); + builder + .setPlayerA(config.playerA) + .setPlayerB(config.playerB) + .setStatus(config.status); + + if (config.requester) { + builder.setRequester(config.requester); + } + createdFriendships.push(builder.build()); + } + }); + + beforeEach(async () => { + friendshipService = await FriendshipModule.getFriendshipService(); + await friendshipModel.deleteMany({}); + await clanModel.deleteMany({}); + await playerModel.deleteMany({}); + + await createMockClans([{ _id: clan_id, name: 'TestClan' }]); + await createMockPlayers([ + { + _id: player1_id, + name: 'Player1', + clan_id, + uniqueIdentifier: 'unique-1', + }, + { + _id: player2_id, + name: 'Player2', + clan_id, + uniqueIdentifier: 'unique-2', + }, + { + _id: player3_id, + name: 'Player3', + clan_id, + uniqueIdentifier: 'unique-3', + }, + ]); + await friendshipModel.create(createdFriendships); + }); + + it('Should send notifications for all pending friendship requests', async () => { + const pendingFriendships = await friendshipModel.find({ + status: FriendshipStatus.PENDING, + }); + + expect(pendingFriendships).toHaveLength(3); + + const notifier = (friendshipService as any).notifier; + const spy = jest.spyOn(notifier, 'newFriendRequest'); + + for (const friendship of pendingFriendships) { + // void function + const result = + await friendshipService.sendNewFriendRequestNotification(friendship); + expect(result).toBeUndefined(); + } + + expect(spy).toHaveBeenCalledTimes(3); + spy.mockRestore(); + }); +}); From c60db515d9c79d35c403f5ee856f142b860ad2d7 Mon Sep 17 00:00:00 2001 From: hoan301298 Date: Wed, 17 Dec 2025 18:03:05 +0200 Subject: [PATCH 15/18] add getFriendshipsWithStatus test --- .../getFriendshipsWithStatus.test.ts | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 src/__tests__/friendship/friendshipService/getFriendshipsWithStatus.test.ts diff --git a/src/__tests__/friendship/friendshipService/getFriendshipsWithStatus.test.ts b/src/__tests__/friendship/friendshipService/getFriendshipsWithStatus.test.ts new file mode 100644 index 000000000..8f47138fd --- /dev/null +++ b/src/__tests__/friendship/friendshipService/getFriendshipsWithStatus.test.ts @@ -0,0 +1,126 @@ +import { ObjectId } from "mongodb"; +import { FriendshipService } from "../../../friendship/friendship.service" +import FriendshipModule from "../modules/friendship.module"; +import { Friendship } from "../../../friendship/friendship.schema"; +import { FriendshipStatus } from "../../../friendship/enum/friendship-status.enum"; +import FriendshipBuilderFactory from "../data/friendshipBuilderFactory"; + +describe('FriendshipService.getFriendshipsWithStatus() test suites', () => { + let friendshipService: FriendshipService; + const friendshipModel = FriendshipModule.getFriendshipModel(); + const createdFriendships: Friendship[] = []; + + const player1_id = new ObjectId().toString(); + const player2_id = new ObjectId().toString(); + const player3_id = new ObjectId().toString(); + const player4_id = new ObjectId().toString(); + + beforeAll(() => { + const friendshipConfigs = [ + { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player1_id, + playerB: player3_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player1_id, + playerB: player4_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player2_id, + playerB: player3_id, + status: FriendshipStatus.ACCEPTED, + }, + { + playerA: player2_id, + playerB: player4_id, + status: FriendshipStatus.ACCEPTED, + }, + { + playerA: player3_id, + playerB: player4_id, + status: FriendshipStatus.BLOCKED, + }, + ]; + + for (const config of friendshipConfigs) { + const builder = FriendshipBuilderFactory.getBuilder('Friendship'); + builder + .setPlayerA(config.playerA) + .setPlayerB(config.playerB) + .setStatus(config.status); + + if (config.requester) { + builder.setRequester(config.requester); + } + createdFriendships.push(builder.build()); + } + }); + + beforeEach(async () => { + await friendshipModel.deleteMany({}); + friendshipService = await FriendshipModule.getFriendshipService(); + + await friendshipModel.create(createdFriendships); + }); + + it('Should get all friendship with status PENDING', async () => { + const [friendships, err] = await friendshipService.getFriendshipsWithStatus( + player1_id, + FriendshipStatus.PENDING + ); + + expect(err).toBeNull(); + expect(friendships).toHaveLength(3); + }); + + it('Should get all friendship with status ACCEPTED', async () => { + const [friendships, err] = await friendshipService.getFriendshipsWithStatus( + player2_id, + FriendshipStatus.ACCEPTED + ); + + expect(err).toBeNull(); + expect(friendships).toHaveLength(2); + }); + + it('Should get all friendship with status BLOCKED', async () => { + const [friendships, err] = await friendshipService.getFriendshipsWithStatus( + player3_id, + FriendshipStatus.BLOCKED + ); + + expect(err).toBeNull(); + expect(friendships).toHaveLength(1); + }); + + it('Should return NOT_FOUND if no match player_id', async () => { + const randomPlayer_id = new ObjectId().toString(); + const [friendships, err] = await friendshipService.getFriendshipsWithStatus( + randomPlayer_id, + FriendshipStatus.ACCEPTED + ); + + expect(err).toContainSE_NOT_FOUND(); + expect(friendships).toBeNull(); + }); + + it('Should return NOT_FOUND if no match STATUS', async () => { + const [friendships, err] = await friendshipService.getFriendshipsWithStatus( + player1_id, + undefined + ); + + expect(err).toContainSE_NOT_FOUND(); + expect(friendships).toBeNull(); + }); +}) \ No newline at end of file From d377c122315f31119fbccb9e202b991a8e974830 Mon Sep 17 00:00:00 2001 From: hoan301298 Date: Wed, 17 Dec 2025 19:59:22 +0200 Subject: [PATCH 16/18] add test for addFriend --- .../friendshipService/addFriend.test.ts | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/__tests__/friendship/friendshipService/addFriend.test.ts diff --git a/src/__tests__/friendship/friendshipService/addFriend.test.ts b/src/__tests__/friendship/friendshipService/addFriend.test.ts new file mode 100644 index 000000000..8dd37361b --- /dev/null +++ b/src/__tests__/friendship/friendshipService/addFriend.test.ts @@ -0,0 +1,116 @@ +import { ObjectId } from 'mongodb'; +import { FriendshipService } from '../../../friendship/friendship.service'; +import FriendshipModule from '../modules/friendship.module'; +import FriendshipBuilderFactory from '../data/friendshipBuilderFactory'; +import { FriendshipStatus } from '../../../friendship/enum/friendship-status.enum'; +import { Friendship } from '../../../friendship/friendship.schema'; +import { + createMockClans, + createMockPlayers, +} from '../data/mockData/createData.mock'; + +describe('FriendshipService.sendNewFriendRequestNotification()', () => { + let friendshipService: FriendshipService; + const clanModel = FriendshipModule.getClanModel(); + const playerModel = FriendshipModule.getPlayerModel(); + const friendshipModel = FriendshipModule.getFriendshipModel(); + const createdFriendships: Friendship[] = []; + + const player1_id = new ObjectId().toString(); + const player2_id = new ObjectId().toString(); + const player3_id = new ObjectId().toString(); + + const clan_id = new ObjectId().toString(); + + beforeAll(async () => { + const friendshipConfigs = [ + { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player1_id, + playerB: player3_id, + status: FriendshipStatus.ACCEPTED, + }, + ]; + + for (const config of friendshipConfigs) { + const builder = FriendshipBuilderFactory.getBuilder('Friendship'); + builder + .setPlayerA(config.playerA) + .setPlayerB(config.playerB) + .setStatus(config.status); + + if (config.requester) { + builder.setRequester(config.requester); + } + createdFriendships.push(builder.build()); + } + }); + + beforeEach(async () => { + friendshipService = await FriendshipModule.getFriendshipService(); + await friendshipModel.deleteMany({}); + await clanModel.deleteMany({}); + await playerModel.deleteMany({}); + + await createMockClans([{ _id: clan_id, name: 'TestClan' }]); + await createMockPlayers([ + { + _id: player1_id, + name: 'Player1', + clan_id, + uniqueIdentifier: 'unique-1', + }, + { + _id: player2_id, + name: 'Player2', + clan_id, + uniqueIdentifier: 'unique-2', + }, + { + _id: player3_id, + name: 'Player3', + clan_id, + uniqueIdentifier: 'unique-3', + }, + ]); + await friendshipModel.create(createdFriendships); + }); + + it('Should return undefined if addFriend is successful', async () => { + // void function if success + await expect(friendshipService.addFriend(player2_id, player3_id)) + .resolves + .toBeUndefined(); + }); + + it( + 'Should return NOT_UNIQUE from pairkey if 2 players have friendship with status PENDING', + async () => { + const [friendship, err] = await friendshipService.addFriend( + player1_id, + player2_id + ); + + expect(err).toContainSE_NOT_UNIQUE(); + expect(friendship).toBeNull(); + } + ); + + it( + 'Should return NOT_UNIQUE from pairkey if 2 players have friendship with status ACCEPTED', + async () => { + const [friendship, err] = await friendshipService.addFriend( + player1_id, + player3_id + ); + + expect(err).toContainSE_NOT_UNIQUE(); + expect(friendship).toBeNull(); + } + ); +}); From e762b242934584ba4063515312acb1c2604f7668 Mon Sep 17 00:00:00 2001 From: hoan301298 Date: Fri, 19 Dec 2025 13:42:12 +0200 Subject: [PATCH 17/18] remove beforeAll and simplify beforeEach in tests with createMockFriendships --- .../data/mockData/createData.mock.ts | 38 +++++--- .../friendshipService/addFriend.test.ts | 56 ++++------- .../getFriendRequests.test.ts | 95 ++++++++----------- .../getFriendshipsWithStatus.test.ts | 94 ++++++++---------- .../getPlayerFriendlist.test.ts | 65 ++++++------- .../sendNewFriendRequestNotification.test.ts | 66 +++++-------- 6 files changed, 166 insertions(+), 248 deletions(-) diff --git a/src/__tests__/friendship/data/mockData/createData.mock.ts b/src/__tests__/friendship/data/mockData/createData.mock.ts index 42f1e1aed..ed5e4e318 100644 --- a/src/__tests__/friendship/data/mockData/createData.mock.ts +++ b/src/__tests__/friendship/data/mockData/createData.mock.ts @@ -3,12 +3,29 @@ import FriendshipModule from '../../modules/friendship.module'; import PlayerBuilder from '../../../player/data/player/playerBuilder'; import { Clan } from '../../../../clan/clan.schema'; import ClanBuilder from '../../../clan/data/clan/ClanBuilder'; +import { Friendship } from 'src/friendship/friendship.schema'; +import FriendshipBuilderFactory from '../friendshipBuilderFactory'; + +export async function createMockFriendships( + overides: Partial[] +): Promise { + const model = FriendshipModule.getFriendshipModel(); + + for (const overide of overides) { + const builder = FriendshipBuilderFactory.getBuilder('Friendship') + .setPlayerA(overide.playerA) + .setPlayerB(overide.playerB) + .setStatus(overide.status); + + if (overide.requester) builder.setRequester(overide.requester); + await model.create(builder.build()); + } +} export async function createMockPlayers( - overides: Partial[], -): Promise { + overides: Partial[] +): Promise { const model = FriendshipModule.getPlayerModel(); - const players: Player[] = []; for (const overide of overides) { const builder = new PlayerBuilder() @@ -18,18 +35,14 @@ export async function createMockPlayers( .setClanId(overide.clan_id) .build(); - const savedPlayer = await model.create(builder); - players.push(savedPlayer); + await model.create(builder); } - - return players; } export async function createMockClans( - overides: Partial[], -): Promise { + overides: Partial[] +): Promise { const model = FriendshipModule.getClanModel(); - const clans: Clan[] = []; for (const overide of overides) { const builder = new ClanBuilder() @@ -37,9 +50,6 @@ export async function createMockClans( .setName(overide.name) .build(); - const savedClans = await model.create(builder); - clans.push(savedClans); + await model.create(builder); } - - return clans; } diff --git a/src/__tests__/friendship/friendshipService/addFriend.test.ts b/src/__tests__/friendship/friendshipService/addFriend.test.ts index 8dd37361b..bbfaf3522 100644 --- a/src/__tests__/friendship/friendshipService/addFriend.test.ts +++ b/src/__tests__/friendship/friendshipService/addFriend.test.ts @@ -1,20 +1,16 @@ import { ObjectId } from 'mongodb'; import { FriendshipService } from '../../../friendship/friendship.service'; import FriendshipModule from '../modules/friendship.module'; -import FriendshipBuilderFactory from '../data/friendshipBuilderFactory'; import { FriendshipStatus } from '../../../friendship/enum/friendship-status.enum'; -import { Friendship } from '../../../friendship/friendship.schema'; import { createMockClans, + createMockFriendships, createMockPlayers, } from '../data/mockData/createData.mock'; +import { Friendship } from 'src/friendship/friendship.schema'; describe('FriendshipService.sendNewFriendRequestNotification()', () => { let friendshipService: FriendshipService; - const clanModel = FriendshipModule.getClanModel(); - const playerModel = FriendshipModule.getPlayerModel(); - const friendshipModel = FriendshipModule.getFriendshipModel(); - const createdFriendships: Friendship[] = []; const player1_id = new ObjectId().toString(); const player2_id = new ObjectId().toString(); @@ -22,40 +18,22 @@ describe('FriendshipService.sendNewFriendRequestNotification()', () => { const clan_id = new ObjectId().toString(); - beforeAll(async () => { - const friendshipConfigs = [ - { - playerA: player1_id, - playerB: player2_id, - status: FriendshipStatus.PENDING, - requester: player1_id, - }, - { - playerA: player1_id, - playerB: player3_id, - status: FriendshipStatus.ACCEPTED, - }, - ]; - - for (const config of friendshipConfigs) { - const builder = FriendshipBuilderFactory.getBuilder('Friendship'); - builder - .setPlayerA(config.playerA) - .setPlayerB(config.playerB) - .setStatus(config.status); - - if (config.requester) { - builder.setRequester(config.requester); - } - createdFriendships.push(builder.build()); - } - }); + const friendshipConfigs: Partial[] = [ + { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player1_id, + playerB: player3_id, + status: FriendshipStatus.ACCEPTED, + }, + ]; beforeEach(async () => { friendshipService = await FriendshipModule.getFriendshipService(); - await friendshipModel.deleteMany({}); - await clanModel.deleteMany({}); - await playerModel.deleteMany({}); await createMockClans([{ _id: clan_id, name: 'TestClan' }]); await createMockPlayers([ @@ -78,7 +56,7 @@ describe('FriendshipService.sendNewFriendRequestNotification()', () => { uniqueIdentifier: 'unique-3', }, ]); - await friendshipModel.create(createdFriendships); + await createMockFriendships(friendshipConfigs); }); it('Should return undefined if addFriend is successful', async () => { @@ -113,4 +91,4 @@ describe('FriendshipService.sendNewFriendRequestNotification()', () => { expect(friendship).toBeNull(); } ); -}); +}); \ No newline at end of file diff --git a/src/__tests__/friendship/friendshipService/getFriendRequests.test.ts b/src/__tests__/friendship/friendshipService/getFriendRequests.test.ts index 6cf672522..1a5eeabf8 100644 --- a/src/__tests__/friendship/friendshipService/getFriendRequests.test.ts +++ b/src/__tests__/friendship/friendshipService/getFriendRequests.test.ts @@ -1,74 +1,53 @@ -import { Types } from 'mongoose'; import { FriendshipService } from '../../../friendship/friendship.service'; -import FriendshipBuilderFactory from '../data/friendshipBuilderFactory'; import FriendshipModule from '../modules/friendship.module'; -import { Friendship } from '../../../friendship/friendship.schema'; import { FriendshipStatus } from '../../../friendship/enum/friendship-status.enum'; +import { ObjectId } from 'mongodb'; +import { createMockFriendships } from '../data/mockData/createData.mock'; +import { Friendship } from 'src/friendship/friendship.schema'; describe('Friendship.getFriendRequests() test suites', () => { let friendshipService: FriendshipService; const friendshipModel = FriendshipModule.getFriendshipModel(); - const player1_id = new Types.ObjectId(); - const player2_id = new Types.ObjectId(); - const player3_id = new Types.ObjectId(); - const player4_id = new Types.ObjectId(); - - const createdFriendships: Friendship[] = []; - - beforeAll(() => { - const friendshipConfigs = [ - { - playerA: player1_id, - playerB: player2_id, - status: FriendshipStatus.PENDING, - requester: player1_id, - }, - { - playerA: player1_id, - playerB: player3_id, - status: FriendshipStatus.PENDING, - requester: player1_id, - }, - { - playerA: player2_id, - playerB: player3_id, - status: FriendshipStatus.ACCEPTED, - }, - { - playerA: player1_id, - playerB: player4_id, - status: FriendshipStatus.BLOCKED, - }, - ]; - - for (const config of friendshipConfigs) { - const friendshipBuilder = - FriendshipBuilderFactory.getBuilder('Friendship'); - - friendshipBuilder - .setPlayerA(config.playerA) - .setPlayerB(config.playerB) - .setStatus(config.status); - - if (config.requester) { - friendshipBuilder.setRequester(config.requester); - } - - createdFriendships.push(friendshipBuilder.build()); - } - }); + const player1_id = new ObjectId().toString(); + const player2_id = new ObjectId().toString(); + const player3_id = new ObjectId().toString(); + const player4_id = new ObjectId().toString(); + + const friendshipConfigs: Partial[] = [ + { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player1_id, + playerB: player3_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player2_id, + playerB: player3_id, + status: FriendshipStatus.ACCEPTED, + }, + { + playerA: player1_id, + playerB: player4_id, + status: FriendshipStatus.BLOCKED, + }, + ]; beforeEach(async () => { - await friendshipModel.deleteMany({}); friendshipService = await FriendshipModule.getFriendshipService(); - await friendshipModel.create(createdFriendships); + await createMockFriendships(friendshipConfigs) }); it('Should get two friendship requests for player1', async () => { const [friendships, err] = await friendshipService.getFriendRequests( - player1_id.toString(), + player1_id, ); expect(err).toBeNull(); @@ -77,7 +56,7 @@ describe('Friendship.getFriendRequests() test suites', () => { it('Should get one friendship request for player2', async () => { const [friendships, err] = await friendshipService.getFriendRequests( - player2_id.toString(), + player2_id, ); expect(err).toBeNull(); @@ -86,7 +65,7 @@ describe('Friendship.getFriendRequests() test suites', () => { it('should return NOT_FOUND for player4', async () => { const [friendships, err] = await friendshipService.getFriendRequests( - player4_id.toString(), + player4_id, ); expect(err).toContainSE_NOT_FOUND(); @@ -95,7 +74,7 @@ describe('Friendship.getFriendRequests() test suites', () => { it('Should avoid status ACCEPTED and return one friendship request', async () => { const [friendships, err] = await friendshipService.getFriendRequests( - player3_id.toString(), + player3_id, ); expect(err).toBeNull(); diff --git a/src/__tests__/friendship/friendshipService/getFriendshipsWithStatus.test.ts b/src/__tests__/friendship/friendshipService/getFriendshipsWithStatus.test.ts index 8f47138fd..02a3939a9 100644 --- a/src/__tests__/friendship/friendshipService/getFriendshipsWithStatus.test.ts +++ b/src/__tests__/friendship/friendshipService/getFriendshipsWithStatus.test.ts @@ -1,76 +1,58 @@ import { ObjectId } from "mongodb"; import { FriendshipService } from "../../../friendship/friendship.service" import FriendshipModule from "../modules/friendship.module"; -import { Friendship } from "../../../friendship/friendship.schema"; import { FriendshipStatus } from "../../../friendship/enum/friendship-status.enum"; -import FriendshipBuilderFactory from "../data/friendshipBuilderFactory"; +import { createMockFriendships } from "../data/mockData/createData.mock"; +import { Friendship } from "src/friendship/friendship.schema"; describe('FriendshipService.getFriendshipsWithStatus() test suites', () => { let friendshipService: FriendshipService; - const friendshipModel = FriendshipModule.getFriendshipModel(); - const createdFriendships: Friendship[] = []; const player1_id = new ObjectId().toString(); const player2_id = new ObjectId().toString(); const player3_id = new ObjectId().toString(); const player4_id = new ObjectId().toString(); - beforeAll(() => { - const friendshipConfigs = [ - { - playerA: player1_id, - playerB: player2_id, - status: FriendshipStatus.PENDING, - requester: player1_id, - }, - { - playerA: player1_id, - playerB: player3_id, - status: FriendshipStatus.PENDING, - requester: player1_id, - }, - { - playerA: player1_id, - playerB: player4_id, - status: FriendshipStatus.PENDING, - requester: player1_id, - }, - { - playerA: player2_id, - playerB: player3_id, - status: FriendshipStatus.ACCEPTED, - }, - { - playerA: player2_id, - playerB: player4_id, - status: FriendshipStatus.ACCEPTED, - }, - { - playerA: player3_id, - playerB: player4_id, - status: FriendshipStatus.BLOCKED, - }, - ]; - - for (const config of friendshipConfigs) { - const builder = FriendshipBuilderFactory.getBuilder('Friendship'); - builder - .setPlayerA(config.playerA) - .setPlayerB(config.playerB) - .setStatus(config.status); - - if (config.requester) { - builder.setRequester(config.requester); - } - createdFriendships.push(builder.build()); - } - }); + const friendshipConfigs: Partial[] = [ + { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player1_id, + playerB: player3_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player1_id, + playerB: player4_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player2_id, + playerB: player3_id, + status: FriendshipStatus.ACCEPTED, + }, + { + playerA: player2_id, + playerB: player4_id, + status: FriendshipStatus.ACCEPTED, + }, + { + playerA: player3_id, + playerB: player4_id, + status: FriendshipStatus.BLOCKED, + }, + ]; beforeEach(async () => { - await friendshipModel.deleteMany({}); friendshipService = await FriendshipModule.getFriendshipService(); - await friendshipModel.create(createdFriendships); + await createMockFriendships(friendshipConfigs); }); it('Should get all friendship with status PENDING', async () => { diff --git a/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts b/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts index 422d2d737..016657016 100644 --- a/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts +++ b/src/__tests__/friendship/friendshipService/getPlayerFriendlist.test.ts @@ -1,53 +1,42 @@ import { FriendshipService } from '../../../friendship/friendship.service'; import FriendshipModule from '../modules/friendship.module'; -import FriendshipBuilderFactory from '../data/friendshipBuilderFactory'; -import { Friendship } from '../../../friendship/friendship.schema'; import { FriendshipStatus } from '../../../friendship/enum/friendship-status.enum'; import { ObjectId } from 'mongodb'; +import { createMockFriendships } from '../data/mockData/createData.mock'; +import { Friendship } from 'src/friendship/friendship.schema'; describe('Friendship.getPlayerFriendlist() test suite', () => { let friendshipService: FriendshipService; - const friendshipModel = FriendshipModule.getFriendshipModel(); - const friendshipBuilder = FriendshipBuilderFactory.getBuilder('Friendship'); - const player1_id = new ObjectId(); - const player2_id = new ObjectId(); - const player3_id = new ObjectId(); - - const createdFriendships: Friendship[] = []; - - beforeAll(() => { - const friendshipToCreate1 = friendshipBuilder - .setPlayerA(player1_id) - .setPlayerB(player2_id) - .setStatus(FriendshipStatus.ACCEPTED) - .build(); - - createdFriendships.push(friendshipToCreate1); - - const friendshipToCreate2 = friendshipBuilder - .setPlayerA(player1_id) - .setPlayerB(player3_id) - .setStatus(FriendshipStatus.ACCEPTED) - .build(); - - createdFriendships.push(friendshipToCreate2); - - const friendshipToCreate3 = friendshipBuilder - .setPlayerA(player2_id) - .setPlayerB(player3_id) - .setStatus(FriendshipStatus.PENDING) - .setRequester(player2_id) - .build(); - - createdFriendships.push(friendshipToCreate3); - }); + const player1_id = new ObjectId().toString(); + const player2_id = new ObjectId().toString(); + const player3_id = new ObjectId().toString(); + + const friendshipConfigs: Partial[] = [ + { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.ACCEPTED, + + }, + { + playerA: player1_id, + playerB: player3_id, + status: FriendshipStatus.ACCEPTED, + + }, + { + playerA: player2_id, + playerB: player3_id, + status: FriendshipStatus.PENDING, + requester: player2_id, + }, + ]; beforeEach(async () => { - await friendshipModel.deleteMany({}); friendshipService = await FriendshipModule.getFriendshipService(); - await friendshipModel.create(createdFriendships); + await createMockFriendships(friendshipConfigs); }); it('Should get two friendships for player1', async () => { diff --git a/src/__tests__/friendship/friendshipService/sendNewFriendRequestNotification.test.ts b/src/__tests__/friendship/friendshipService/sendNewFriendRequestNotification.test.ts index fba1e0c97..36308e26e 100644 --- a/src/__tests__/friendship/friendshipService/sendNewFriendRequestNotification.test.ts +++ b/src/__tests__/friendship/friendshipService/sendNewFriendRequestNotification.test.ts @@ -3,18 +3,16 @@ import { FriendshipService } from '../../../friendship/friendship.service'; import FriendshipModule from '../modules/friendship.module'; import FriendshipBuilderFactory from '../data/friendshipBuilderFactory'; import { FriendshipStatus } from '../../../friendship/enum/friendship-status.enum'; -import { Friendship } from '../../../friendship/friendship.schema'; import { createMockClans, + createMockFriendships, createMockPlayers, } from '../data/mockData/createData.mock'; +import { Friendship } from 'src/friendship/friendship.schema'; describe('FriendshipService.sendNewFriendRequestNotification()', () => { let friendshipService: FriendshipService; - const clanModel = FriendshipModule.getClanModel(); - const playerModel = FriendshipModule.getPlayerModel(); const friendshipModel = FriendshipModule.getFriendshipModel(); - const createdFriendships: Friendship[] = []; const player1_id = new ObjectId().toString(); const player2_id = new ObjectId().toString(); @@ -22,47 +20,29 @@ describe('FriendshipService.sendNewFriendRequestNotification()', () => { const clan_id = new ObjectId().toString(); - beforeAll(() => { - const friendshipConfigs = [ - { - playerA: player1_id, - playerB: player2_id, - status: FriendshipStatus.PENDING, - requester: player1_id, - }, - { - playerA: player1_id, - playerB: player3_id, - status: FriendshipStatus.PENDING, - requester: player1_id, - }, - { - playerA: player2_id, - playerB: player3_id, - status: FriendshipStatus.PENDING, - requester: player2_id, - }, - ]; - - for (const config of friendshipConfigs) { - const builder = FriendshipBuilderFactory.getBuilder('Friendship'); - builder - .setPlayerA(config.playerA) - .setPlayerB(config.playerB) - .setStatus(config.status); - - if (config.requester) { - builder.setRequester(config.requester); - } - createdFriendships.push(builder.build()); - } - }); + const friendshipConfigs: Partial[] = [ + { + playerA: player1_id, + playerB: player2_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player1_id, + playerB: player3_id, + status: FriendshipStatus.PENDING, + requester: player1_id, + }, + { + playerA: player2_id, + playerB: player3_id, + status: FriendshipStatus.PENDING, + requester: player2_id, + }, + ]; beforeEach(async () => { friendshipService = await FriendshipModule.getFriendshipService(); - await friendshipModel.deleteMany({}); - await clanModel.deleteMany({}); - await playerModel.deleteMany({}); await createMockClans([{ _id: clan_id, name: 'TestClan' }]); await createMockPlayers([ @@ -85,7 +65,7 @@ describe('FriendshipService.sendNewFriendRequestNotification()', () => { uniqueIdentifier: 'unique-3', }, ]); - await friendshipModel.create(createdFriendships); + await createMockFriendships(friendshipConfigs); }); it('Should send notifications for all pending friendship requests', async () => { From b8246dc414a77ba5ce85c01b852bee48be49b274 Mon Sep 17 00:00:00 2001 From: CapoMK25 Date: Sat, 10 Jan 2026 14:16:47 +0200 Subject: [PATCH 18/18] fix(webdav): add dynamic import helper to support ESM in CJS context (#751) --- package-lock.json | 318 +++++++++++-------------- package.json | 4 +- src/common/utils/createWebDavClient.ts | 14 ++ src/gameAnalytics/logFile.service.ts | 113 ++------- src/profile/dto/playerProfile.dto.ts | 3 +- tsconfig.build.json | 9 +- tsconfig.json | 48 ++-- 7 files changed, 213 insertions(+), 296 deletions(-) create mode 100644 src/common/utils/createWebDavClient.ts diff --git a/package-lock.json b/package-lock.json index 8b205f4eb..fc7f106e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "mongoose": "8.19.4", "mqtt": "5.14.1", "rxjs": "7.8.2", - "webdav": "5.8.0", + "webdav": "^4.11.2", "ws": "8.18.3" }, "devDependencies": { @@ -737,15 +737,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/@buttercup/fetch": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.2.1.tgz", - "integrity": "sha512-sCgECOx8wiqY8NN1xN22BqqKzXYIG2AicNLlakOAI4f0WgyLVUbAigMf8CZhBtJxdudTcB1gD5lciqi44jwJvg==", - "license": "MIT", - "optionalDependencies": { - "node-fetch": "^3.3.0" - } - }, "node_modules/@casl/ability": { "version": "6.7.3", "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.7.3.tgz", @@ -4206,6 +4197,20 @@ "tslib": "^2.4.0" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, "node_modules/b4a": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", @@ -4344,8 +4349,7 @@ "node_modules/base-64": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", - "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", - "license": "MIT" + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -4637,8 +4641,7 @@ "node_modules/byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/byte-length/-/byte-length-1.0.2.tgz", - "integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==", - "license": "MIT" + "integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==" }, "node_modules/bytes": { "version": "3.1.2", @@ -4757,7 +4760,6 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "license": "BSD-3-Clause", "engines": { "node": "*" } @@ -4993,6 +4995,17 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -5248,20 +5261,10 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "license": "BSD-3-Clause", "engines": { "node": "*" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -5324,6 +5327,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -5470,18 +5481,6 @@ "node": ">=10.13.0" } }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5529,6 +5528,20 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -6049,21 +6062,18 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz", + "integrity": "sha512-FTFVjYoBOZTJekiUsawGsSYV9QL0A+zDYCRj7y34IO6Jg+2IMYEtQa+bbictpdpV8dHxXywqU7C0gRDEOFtBFg==", "dependencies": { - "strnum": "^1.1.1" + "strnum": "^1.0.4" }, "bin": { - "fxparser": "src/cli/cli.js" + "xml2js": "cli.js" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" } }, "node_modules/fastq": { @@ -6086,29 +6096,6 @@ "bser": "2.1.1" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -6262,7 +6249,6 @@ "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "dev": true, "funding": [ { "type": "individual", @@ -6324,16 +6310,38 @@ "webpack": "^5.11.0" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dependencies": { - "fetch-blob": "^3.1.2" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" }, "engines": { - "node": ">=12.20.0" + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" } }, "node_modules/forwarded": { @@ -6662,6 +6670,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -6674,6 +6696,14 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, "node_modules/help-me": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", @@ -6681,10 +6711,9 @@ "license": "MIT" }, "node_modules/hot-patcher": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-2.0.1.tgz", - "integrity": "sha512-ECg1JFG0YzehicQaogenlcs2qg6WsXQsxtnbr1i696u5tLUjtJdQAh0u2g0Q5YV45f263Ta1GnUJsc8WIfJf4Q==", - "license": "MIT" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-1.0.0.tgz", + "integrity": "sha512-3H8VH0PreeNsKMZw16nTHbUp4YoHCnPlawpsPXGJUR4qENDynl79b6Xk9CIFvLcH1qungBsCuzKcWyzoPPalTw==" }, "node_modules/html-escaper": { "version": "2.0.2", @@ -6941,8 +6970,7 @@ "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "license": "MIT" + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "node_modules/is-extglob": { "version": "2.1.1", @@ -8106,10 +8134,9 @@ } }, "node_modules/layerr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/layerr/-/layerr-3.0.0.tgz", - "integrity": "sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA==", - "license": "MIT" + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/layerr/-/layerr-0.1.2.tgz", + "integrity": "sha512-ob5kTd9H3S4GOG2nVXyQhOu9O8nBgP555XxWPkJI0tR0JeRilfyTp8WtPdIJHLXBmHMSdEq5+KMxiYABeScsIQ==" }, "node_modules/leven": { "version": "3.1.0", @@ -8359,7 +8386,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "license": "BSD-3-Clause", "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", @@ -8888,8 +8914,7 @@ "node_modules/nested-property": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/nested-property/-/nested-property-4.0.0.tgz", - "integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==", - "license": "MIT" + "integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==" }, "node_modules/new-find-package-json": { "version": "2.0.0", @@ -8919,26 +8944,6 @@ "node": "^18 || ^20 || >= 21" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -8949,24 +8954,6 @@ "lodash": "^4.17.21" } }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -9462,8 +9449,7 @@ "node_modules/path-posix": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", - "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==", - "license": "ISC" + "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==" }, "node_modules/path-scurry": { "version": "2.0.0", @@ -9761,8 +9747,7 @@ "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "license": "MIT" + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -9916,8 +9901,7 @@ "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "license": "MIT" + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, "node_modules/resolve-cwd": { "version": "3.0.0", @@ -10660,8 +10644,7 @@ "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } - ], - "license": "MIT" + ] }, "node_modules/strtok3": { "version": "10.3.4", @@ -11437,19 +11420,14 @@ } }, "node_modules/url-join": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", - "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "license": "MIT", "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -11544,62 +11522,46 @@ "defaults": "^1.0.3" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/webdav": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webdav/-/webdav-5.8.0.tgz", - "integrity": "sha512-iuFG7NamJ41Oshg4930iQgfIpRrUiatPWIekeznYgEf2EOraTRcDPTjy7gIOMtkdpKTaqPk1E68NO5PAGtJahA==", - "license": "MIT", + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/webdav/-/webdav-4.11.2.tgz", + "integrity": "sha512-Ht9TPD5EB7gYW0YmhRcE5NW0/dn/HQfyLSPQY1Rw1coQ5MQTUooAQ9Bpqt4EU7QLw0b95tX4cU59R+SIojs9KQ==", "dependencies": { - "@buttercup/fetch": "^0.2.1", + "axios": "^0.27.2", "base-64": "^1.0.0", "byte-length": "^1.0.2", - "entities": "^6.0.0", - "fast-xml-parser": "^4.5.1", - "hot-patcher": "^2.0.1", - "layerr": "^3.0.0", + "fast-xml-parser": "^3.19.0", + "he": "^1.2.0", + "hot-patcher": "^1.0.0", + "layerr": "^0.1.2", "md5": "^2.3.0", - "minimatch": "^9.0.5", + "minimatch": "^5.1.0", "nested-property": "^4.0.0", - "node-fetch": "^3.3.2", "path-posix": "^1.0.0", - "url-join": "^5.0.0", + "url-join": "^4.0.1", "url-parse": "^1.5.10" }, "engines": { - "node": ">=14" + "node": ">=10" } }, "node_modules/webdav/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/webdav/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=10" } }, "node_modules/webidl-conversions": { diff --git a/package.json b/package.json index 688be8bfc..c9482a259 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "mongoose": "8.19.4", "mqtt": "5.14.1", "rxjs": "7.8.2", - "webdav": "5.8.0", + "webdav": "^4.11.2", "ws": "8.18.3" }, "devDependencies": { @@ -81,4 +81,4 @@ "typescript": "5.9.3", "typescript-eslint": "8.46.4" } -} \ No newline at end of file +} diff --git a/src/common/utils/createWebDavClient.ts b/src/common/utils/createWebDavClient.ts new file mode 100644 index 000000000..b0e6fc515 --- /dev/null +++ b/src/common/utils/createWebDavClient.ts @@ -0,0 +1,14 @@ +// ESM-only workaround: webdav is ESM-only, so we use dynamic import to avoid CommonJS issues +import { createClient, WebDAVClient } from 'webdav'; +import { envVars } from '../service/envHandler/envVars'; + +export function createWebDavClient(): WebDAVClient { + return createClient( + `http://${envVars.OWNCLOUD_HOST}:${envVars.OWNCLOUD_PORT}/remote.php/webdav/`, + { + username: envVars.OWNCLOUD_USER, + password: envVars.OWNCLOUD_PASSWORD, + maxBodyLength: 52428800, + }, + ); +} diff --git a/src/gameAnalytics/logFile.service.ts b/src/gameAnalytics/logFile.service.ts index ed829b529..05464d9a2 100644 --- a/src/gameAnalytics/logFile.service.ts +++ b/src/gameAnalytics/logFile.service.ts @@ -1,29 +1,26 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { createClient, WebDAVClient } from 'webdav'; +import { Readable } from 'stream'; import ServiceError from '../common/service/basicService/ServiceError'; import { SEReason } from '../common/service/basicService/SEReason'; -import { createClient, WebDAVClient } from 'webdav'; import { envVars } from '../common/service/envHandler/envVars'; -import { Readable } from 'stream'; @Injectable() -export class LogFileService { - constructor() { - this.initializeWebDavClient(); - } - private client: WebDAVClient; - +export class LogFileService implements OnModuleInit { + private client!: WebDAVClient; private readonly logFilesRootFolder = envVars.OWNCLOUD_LOG_FILES_ROOT; - /** - * Saves the provided file to the own cloud via WebDAV in the designated folder. - * - * @param fileToSave - The file to save. - * @param player_id - The player's unique identifier to be included in the file name. - * @param battleId - The battle id to which battle belongs to - * @returns A tuple with first element set to _true_ if file was saved - * - * or _array of ServiceErrors_ as a second element - */ + onModuleInit() { + this.client = createClient( + `http://${envVars.OWNCLOUD_HOST}:${envVars.OWNCLOUD_PORT}/remote.php/webdav/`, + { + username: envVars.OWNCLOUD_USER, + password: envVars.OWNCLOUD_PASSWORD, + maxBodyLength: 52428800, + }, + ); + } + async saveFile( fileToSave: Express.Multer.File, player_id: string, @@ -36,7 +33,6 @@ export class LogFileService { try { const isFileFolderExist = await this.client.exists(folderPath); if (!isFileFolderExist) - //Notice that the "logFilesRootFolder" folder must be already created manually in own cloud await this.client.createDirectory(folderPath, { recursive: true }); } catch (error) { return [ @@ -44,7 +40,7 @@ export class LogFileService { [ new ServiceError({ reason: SEReason.UNEXPECTED, - message: 'Unexpected error happen during folder creation', + message: 'Unexpected error during folder creation', additional: this.getWebDavErrorData(error), }), ], @@ -53,13 +49,9 @@ export class LogFileService { try { const fileStream = this.bufferToStream(fileToSave.buffer); - const isSuccess = await this.client.putFileContents( - filePath, - fileStream, - { - overwrite: true, - }, - ); + const isSuccess = await this.client.putFileContents(filePath, fileStream, { + overwrite: true, + }); if (!isSuccess) return [ @@ -79,7 +71,7 @@ export class LogFileService { [ new ServiceError({ reason: SEReason.UNEXPECTED, - message: 'Unexpected error happen during file saving', + message: 'Unexpected error during file saving', additional: this.getWebDavErrorData(error), }), ], @@ -87,11 +79,6 @@ export class LogFileService { } } - /** - * Converts provided buffer of a file to reading stream - * @param buffer buffer to convert - * @returns stream - */ private bufferToStream(buffer: Buffer) { const readable = new Readable(); readable._read = () => {}; @@ -100,89 +87,37 @@ export class LogFileService { return readable; } - /** - * Initializes the WebDAV client using credentials from the environment variables. - */ - private initializeWebDavClient() { - this.client = createClient( - `http://${envVars.OWNCLOUD_HOST}:${envVars.OWNCLOUD_PORT}/remote.php/webdav/`, - { - username: envVars.OWNCLOUD_USER, - password: envVars.OWNCLOUD_PASSWORD, - maxBodyLength: 52428800, - }, - ); - } - - /** - * Extracts the error data from the WebDAV error response. - * - * @param error - The error object to extract information from. - * @returns The response data from the WebDAV error, or null if not available. - */ private getWebDavErrorData(error: any) { return error?.response?.data ?? null; } - /** - * Constructs the full path to the folder where log files will be stored. - * - * @param battleId - The id of the battle where file belongs to - * @returns The full folder path as a string. - */ private getFolderPath(battleId: string) { const folderDataName = this.getDateFolderName(); return `${this.logFilesRootFolder}/${folderDataName}/${battleId}`; } - /** - * Constructs the full path to the file, including the folder and the file name. - * - * @param battleId - The id of the battle where file belongs to - * @param player_id - The player's unique identifier to be included in the file name. - * @returns The full file path. - */ + private getFilePath(battleId: string, player_id: string) { const folderPath = this.getFolderPath(battleId); const fileName = this.getFileName(player_id); return `${folderPath}/${fileName}`; } - /** - * Generates a folder name based on the current date. - * - * @returns A string representing the folder name, formatted as DD-MM-YYYY. - */ private getDateFolderName() { return this.getDateString(); } - /** - * Generates a file name based on the current date, time, player _id, and a random string. - * - * @param player_id - The player's unique identifier to be included in the file name. - * @returns The file name as a string, formatted as DD-MM-YYYY_HH-MM-SS_playerID_random.log. - */ + private getFileName(player_id: string) { const dateString = this.getDateString(); const timeString = this.getTimeString(); const randomString = Math.floor(Math.random() * 1000000); - return `${dateString}_${timeString}_${player_id}_${randomString}.log`; } - /** - * Gets the current date as a string formatted as DD-MM-YYYY. - * - * @returns A string representing the current date. - */ private getDateString() { const now = new Date(); return `${now.getDate()}-${now.getMonth() + 1}-${now.getFullYear()}`; } - /** - * Gets the current time as a string formatted as HH-MM-SS. - * - * @returns A string representing the current time. - */ + private getTimeString() { const now = new Date(); return `${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}`; diff --git a/src/profile/dto/playerProfile.dto.ts b/src/profile/dto/playerProfile.dto.ts index 84c4bd8bd..f1f7abb30 100644 --- a/src/profile/dto/playerProfile.dto.ts +++ b/src/profile/dto/playerProfile.dto.ts @@ -13,5 +13,6 @@ export class PlayerProfileDto extends CreatePlayerDto { @IsProfileExists() @IsMongoId() @IsOptional() - override profile_id: string; + override profile_id = ''; + } diff --git a/tsconfig.build.json b/tsconfig.build.json index bdc5395c5..4dbf13ab2 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -4,12 +4,15 @@ "types": ["node", "multer"], "noEmitOnError": true, "declaration": false, - "sourceMap": false + "sourceMap": false, + "outDir": "./dist", + "rootDir": "./src" }, "exclude": [ "node_modules", "**/*.test.ts", "**/*.spec.ts", "**/__tests__/**" - ] -} \ No newline at end of file + ], + "include": ["src/**/*"] +} diff --git a/tsconfig.json b/tsconfig.json index e648f8513..98cfb48f2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,37 +1,39 @@ { "compilerOptions": { /* Language and Environment */ - "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + "target": "ES2021", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, /* Modules */ - "module": "CommonJS", /* Specify what module code is generated. */ - "rootDir": "./src", /* Specify the root folder within your source files. */ - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - "resolveJsonModule": true, /* Enable importing .json files. */ + "module": "CommonJS", + "moduleResolution": "node", + "moduleDetection": "force", + "rootDir": "./src", + "baseUrl": "./", + "resolveJsonModule": true, /* Emit */ - "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - "outDir": "./dist", /* Specify an output folder for all emitted files. */ - "removeComments": true, /* Disable emitting comments. */ + "declaration": true, + "sourceMap": true, + "outDir": "./dist", + "removeComments": true, /* Interop Constraints */ - "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - "forceConsistentCasingInFileNames": false, /* Ensure that casing is correct in imports. */ + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - "strictNullChecks": false, /* When type checking, take into account 'null' and 'undefined'. */ - "strictBindCallApply": false, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - "noFallthroughCasesInSwitch": false, /* Enable error reporting for fallthrough cases in switch statements. */ + "strict": true, + "noImplicitAny": false, + "strictNullChecks": false, + "strictBindCallApply": false, + "noFallthroughCasesInSwitch": false, /* Completeness */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "skipLibCheck": true }, - "include": ["src/**/*.ts"] -} \ No newline at end of file + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +}