Skip to content

Commit

Permalink
BC-5836 - Add news to KNLDeletion Module (#4654)
Browse files Browse the repository at this point in the history
* add logger to all modules called from deletion UC

* Update apps/server/src/modules/learnroom/service/course.service.spec.ts

Co-authored-by: Bartosz Nowicki <[email protected]>

* add additional metadata to logs during deletion user data for services

* add tests

* fix tests

* fix test coverage

* add new logging funct. to news module

* Update apps/server/src/shared/repo/news/news.repo.integration.spec.ts

Co-authored-by: virgilchiriac <[email protected]>

---------

Co-authored-by: Bartosz Nowicki <[email protected]>
Co-authored-by: sszafGCA <[email protected]>
Co-authored-by: Szymon Szafoni <[email protected]>
Co-authored-by: virgilchiriac <[email protected]>
  • Loading branch information
5 people authored Feb 6, 2024
1 parent f80f9b2 commit e2f3388
Show file tree
Hide file tree
Showing 14 changed files with 385 additions and 13 deletions.
2 changes: 2 additions & 0 deletions apps/server/src/modules/deletion/deletion-api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { FilesStorageClientModule } from '@modules/files-storage-client';
import { DeletionRequestsController } from './controller/deletion-requests.controller';
import { DeletionExecutionsController } from './controller/deletion-executions.controller';
import { DeletionRequestUc } from './uc';
import { NewsModule } from '../news';

@Module({
imports: [
Expand All @@ -37,6 +38,7 @@ import { DeletionRequestUc } from './uc';
RegistrationPinModule,
FilesStorageClientModule,
TaskModule,
NewsModule,
RocketChatModule.forRoot({
uri: Configuration.get('ROCKET_CHAT_URI') as string,
adminId: Configuration.get('ROCKET_CHAT_ADMIN_ID') as string,
Expand Down
21 changes: 20 additions & 1 deletion apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { FilesStorageClientAdapterService } from '@src/modules/files-storage-cli
import { DomainName, OperationType } from '@shared/domain/types';
import { TaskService } from '@modules/task';
import { DomainOperationBuilder } from '@shared/domain/builder';
import { NewsService } from '@src/modules/news/service/news.service';
import { DeletionStatusModel } from '../domain/types';
import { DeletionLogService } from '../services/deletion-log.service';
import { DeletionRequestService } from '../services';
Expand Down Expand Up @@ -47,6 +48,7 @@ describe(DeletionRequestUc.name, () => {
let filesStorageClientAdapterService: DeepMocked<FilesStorageClientAdapterService>;
let dashboardService: DeepMocked<DashboardService>;
let taskService: DeepMocked<TaskService>;
let newsService: DeepMocked<NewsService>;

beforeAll(async () => {
module = await Test.createTestingModule({
Expand Down Expand Up @@ -124,6 +126,7 @@ describe(DeletionRequestUc.name, () => {
provide: TaskService,
useValue: createMock<TaskService>(),
},
{ provide: NewsService, useValue: createMock<NewsService>() },
],
}).compile();

Expand All @@ -145,6 +148,7 @@ describe(DeletionRequestUc.name, () => {
filesStorageClientAdapterService = module.get(FilesStorageClientAdapterService);
dashboardService = module.get(DashboardService);
taskService = module.get(TaskService);
newsService = module.get(NewsService);
await setupEntities();
});

Expand Down Expand Up @@ -241,6 +245,10 @@ describe(DeletionRequestUc.name, () => {
new ObjectId().toHexString(),
]);

const newsUpdated = DomainOperationBuilder.build(DomainName.LESSONS, OperationType.UPDATE, 1, [
new ObjectId().toHexString(),
]);

const parentEmail = '[email protected]';

const pseudonymsDeleted = DomainOperationBuilder.build(DomainName.PSEUDONYMS, OperationType.DELETE, 1, [
Expand Down Expand Up @@ -307,6 +315,7 @@ describe(DeletionRequestUc.name, () => {
taskService.removeCreatorIdFromTasks.mockResolvedValueOnce(tasksModifiedByRemoveCreatorId);
taskService.removeUserFromFinished.mockResolvedValueOnce(tasksModifiedByRemoveUserFromFinished);
taskService.deleteTasksByOnlyCreator.mockResolvedValueOnce(tasksDeleted);
newsService.deleteCreatorOrUpdaterReference.mockResolvedValueOnce(newsUpdated);

return {
deletionRequestToExecute,
Expand Down Expand Up @@ -534,14 +543,24 @@ describe(DeletionRequestUc.name, () => {
expect(taskService.removeUserFromFinished).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId);
});

it('should call newsService.deleteCreatorOrUpdaterReference to update News without creatorId', async () => {
const { deletionRequestToExecute } = setup();

deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]);

await uc.executeDeletionRequests();

expect(newsService.deleteCreatorOrUpdaterReference).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId);
});

it('should call deletionLogService.createDeletionLog to create logs for deletionRequest', async () => {
const { deletionRequestToExecute } = setup();

deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]);

await uc.executeDeletionRequests();

expect(deletionLogService.createDeletionLog).toHaveBeenCalledTimes(12);
expect(deletionLogService.createDeletionLog).toHaveBeenCalledTimes(13);
});
});

Expand Down
18 changes: 17 additions & 1 deletion apps/server/src/modules/deletion/uc/deletion-request.uc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { FilesStorageClientAdapterService } from '@modules/files-storage-client'
import { TaskService } from '@modules/task';
import { DomainOperation } from '@shared/domain/interface';
import { DomainOperationBuilder } from '@shared/domain/builder/domain-operation.builder';
import { NewsService } from '@src/modules/news/service/news.service';
import { DeletionRequestLogResponseBuilder, DeletionTargetRefBuilder } from '../builder';
import { DeletionRequestBodyProps, DeletionRequestLogResponse, DeletionRequestResponse } from '../controller/dto';
import { DeletionRequest, DeletionLog } from '../domain';
Expand All @@ -41,7 +42,8 @@ export class DeletionRequestUc {
private readonly registrationPinService: RegistrationPinService,
private readonly filesStorageClientAdapterService: FilesStorageClientAdapterService,
private readonly dashboardService: DashboardService,
private readonly taskService: TaskService
private readonly taskService: TaskService,
private readonly newsService: NewsService
) {
this.logger.setContext(DeletionRequestUc.name);
}
Expand Down Expand Up @@ -111,6 +113,7 @@ export class DeletionRequestUc {
this.removeUserFromRocketChat(deletionRequest),
this.removeUsersDashboard(deletionRequest),
this.removeUserFromTasks(deletionRequest),
this.removeUsersDataFromNews(deletionRequest),
]);
await this.deletionRequestService.markDeletionRequestAsExecuted(deletionRequest.id);
} catch (error) {
Expand Down Expand Up @@ -370,4 +373,17 @@ export class DeletionRequestUc {
modifiedTasksRef
);
}

private async removeUsersDataFromNews(deletionRequest: DeletionRequest) {
this.logger.debug({ action: 'removeUsersDataFromNews', deletionRequest });
const newsModified = await this.newsService.deleteCreatorOrUpdaterReference(deletionRequest.targetRefId);

await this.logDeletion(
deletionRequest,
newsModified.domain,
newsModified.operation,
newsModified.count,
newsModified.refs
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export class NewsResponse {
@ApiProperty({
description: 'Reference to the User that created the News entity',
})
creator: UserInfoResponse;
creator?: UserInfoResponse;

@ApiPropertyOptional({
description: 'Reference to the User that updated the News entity',
Expand Down
5 changes: 3 additions & 2 deletions apps/server/src/modules/news/mapper/news.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export class NewsMapper {
static mapToResponse(news: News): NewsResponse {
const target = TargetInfoMapper.mapToResponse(news.target);
const school = SchoolInfoMapper.mapToResponse(news.school);
const creator = UserInfoMapper.mapToResponse(news.creator);

const dto = new NewsResponse({
id: news.id,
Expand All @@ -23,12 +22,14 @@ export class NewsMapper {
targetModel: news.targetModel,
target,
school,
creator,
createdAt: news.createdAt,
updatedAt: news.updatedAt,
permissions: news.permissions,
});

if (news.creator) {
dto.creator = UserInfoMapper.mapToResponse(news.creator);
}
if (news.updater) {
dto.updater = UserInfoMapper.mapToResponse(news.updater);
}
Expand Down
5 changes: 3 additions & 2 deletions apps/server/src/modules/news/news.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import { AuthorizationModule } from '@modules/authorization';
import { NewsController } from './controller/news.controller';
import { TeamNewsController } from './controller/team-news.controller';
import { NewsUc } from './uc/news.uc';
import { NewsService } from './service/news.service';

@Module({
imports: [AuthorizationModule, LoggerModule],
controllers: [NewsController, TeamNewsController],
providers: [NewsUc, NewsRepo],
exports: [NewsUc],
providers: [NewsUc, NewsRepo, NewsService],
exports: [NewsUc, NewsService],
})
export class NewsModule {}
151 changes: 151 additions & 0 deletions apps/server/src/modules/news/service/news.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { ObjectId } from '@mikro-orm/mongodb';
import { Test, TestingModule } from '@nestjs/testing';
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { setupEntities, teamNewsFactory, userFactory } from '@shared/testing';
import { Logger } from '@src/core/logger';
import { NewsRepo } from '@shared/repo';
import { DomainOperationBuilder } from '@shared/domain/builder';
import { DomainName, OperationType } from '@shared/domain/types';
import { NewsService } from './news.service';

describe(NewsService.name, () => {
let module: TestingModule;
let service: NewsService;
let repo: DeepMocked<NewsRepo>;

beforeAll(async () => {
module = await Test.createTestingModule({
providers: [
NewsService,
{
provide: NewsRepo,
useValue: createMock<NewsRepo>(),
},
{
provide: Logger,
useValue: createMock<Logger>(),
},
],
}).compile();

service = module.get(NewsService);
repo = module.get(NewsRepo);

await setupEntities();
});

afterEach(() => {
repo.findByCreatorOrUpdaterId.mockClear();
repo.save.mockClear();
});

afterAll(async () => {
await module.close();
});

describe('deleteCreatorReference', () => {
const setup = () => {
const user1 = userFactory.build();
const user2 = userFactory.build();
const anotherUserId = new ObjectId().toHexString();

const news1 = teamNewsFactory.buildWithId({
creator: user1,
});
const news2 = teamNewsFactory.buildWithId({
updater: user2,
});
const news3 = teamNewsFactory.buildWithId({
creator: user1,
updater: user2,
});

const expectedResultWithDeletedCreator = DomainOperationBuilder.build(DomainName.NEWS, OperationType.UPDATE, 2, [
news1.id,
news3.id,
]);

const expectedResultWithDeletedUpdater = DomainOperationBuilder.build(DomainName.NEWS, OperationType.UPDATE, 2, [
news2.id,
news3.id,
]);

const expectedResultWithoutUpdatedNews = DomainOperationBuilder.build(
DomainName.NEWS,
OperationType.UPDATE,
0,
[]
);

return {
anotherUserId,
expectedResultWithDeletedCreator,
expectedResultWithDeletedUpdater,
expectedResultWithoutUpdatedNews,
user1,
user2,
news1,
news2,
news3,
};
};

describe('when user is creator of news', () => {
it('it should be removed from news', async () => {
const { user1, news1, news3 } = setup();

repo.findByCreatorOrUpdaterId.mockResolvedValueOnce([[news1, news3], 2]);

await service.deleteCreatorOrUpdaterReference(user1.id);

expect(news1.creator).toBeUndefined();
expect(news3.creator).toBeUndefined();
});

it('it should return response for 2 news updated', async () => {
const { expectedResultWithDeletedCreator, user1, news1, news3 } = setup();

repo.findByCreatorOrUpdaterId.mockResolvedValueOnce([[news1, news3], 2]);

const result = await service.deleteCreatorOrUpdaterReference(user1.id);

expect(result).toEqual(expectedResultWithDeletedCreator);
});
});

describe('when user is updater of news', () => {
it('user should be removed from updater', async () => {
const { user2, news2, news3 } = setup();

repo.findByCreatorOrUpdaterId.mockResolvedValueOnce([[news2, news3], 2]);

await service.deleteCreatorOrUpdaterReference(user2.id);

expect(news2.updater).toBeUndefined();
expect(news3.updater).toBeUndefined();
});

it('it should return response for 2 news updated', async () => {
const { expectedResultWithDeletedUpdater, user2, news2, news3 } = setup();

repo.findByCreatorOrUpdaterId.mockResolvedValueOnce([[news2, news3], 2]);

const result = await service.deleteCreatorOrUpdaterReference(user2.id);

expect(result).toEqual(expectedResultWithDeletedUpdater);
});
});

describe('when user is neither creator nor updater', () => {
it('should return response with 0 updated news', async () => {
const { anotherUserId, expectedResultWithoutUpdatedNews } = setup();

repo.findByCreatorOrUpdaterId.mockResolvedValueOnce([[], 0]);

const result = await service.deleteCreatorOrUpdaterReference(anotherUserId);

expect(result).toEqual(expectedResultWithoutUpdatedNews);
});
});
});
});
58 changes: 58 additions & 0 deletions apps/server/src/modules/news/service/news.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Injectable } from '@nestjs/common';
import { DomainName, EntityId, OperationType, StatusModel } from '@shared/domain/types';
import { Logger } from '@src/core/logger';
import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable';
import { NewsRepo } from '@shared/repo';
import { DomainOperationBuilder } from '@shared/domain/builder';
import { DomainOperation } from '@shared/domain/interface';
import { News } from '@shared/domain/entity';

@Injectable()
export class NewsService {
constructor(private readonly newsRepo: NewsRepo, private readonly logger: Logger) {
this.logger.setContext(NewsService.name);
}

public async deleteCreatorOrUpdaterReference(userId: EntityId): Promise<DomainOperation> {
this.logger.info(
new DataDeletionDomainOperationLoggable(
'Deleting user data from News',
DomainName.NEWS,
userId,
StatusModel.PENDING
)
);

const [newsWithUserData, counterOfNews] = await this.newsRepo.findByCreatorOrUpdaterId(userId);

newsWithUserData.forEach((newsEntity) => {
newsEntity.removeCreatorReference(userId);
newsEntity.removeUpdaterReference(userId);
});

await this.newsRepo.save(newsWithUserData);

const result = DomainOperationBuilder.build(
DomainName.NEWS,
OperationType.UPDATE,
counterOfNews,
this.getNewsId(newsWithUserData)
);

this.logger.info(
new DataDeletionDomainOperationLoggable(
'Successfully removed user data from News',
DomainName.NEWS,
userId,
StatusModel.FINISHED,
counterOfNews,
0
)
);
return result;
}

private getNewsId(news: News[]): EntityId[] {
return news.map((item) => item.id);
}
}
Loading

0 comments on commit e2f3388

Please sign in to comment.