Skip to content

Commit

Permalink
Merge branch 'main' into N21-1336-change-tool-versioning
Browse files Browse the repository at this point in the history
  • Loading branch information
IgorCapCoder committed Nov 13, 2023
2 parents e12b824 + b11b022 commit f7d65ac
Show file tree
Hide file tree
Showing 57 changed files with 1,613 additions and 2,533 deletions.
29 changes: 21 additions & 8 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,24 @@ module.exports = {
overrides: [
{
files: ['apps/**/*.ts'],
env: {
node: true,
es6: true,
},
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint/eslint-plugin'],
parserOptions: {
project: 'apps/server/tsconfig.lint.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin', 'import'],
extends: [
'airbnb-typescript/base',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'prettier',
'plugin:promise/recommended',
'plugin:import/typescript',
],
parserOptions: {
project: 'apps/server/tsconfig.lint.json',
},
env: {
node: true,
es6: true,
},
rules: {
'import/no-unresolved': 'off', // better handled by ts resolver
'import/no-extraneous-dependencies': 'off', // better handles by ts resolver
Expand All @@ -98,6 +100,17 @@ module.exports = {
allowSingleExtends: true,
},
],
'@typescript-eslint/no-restricted-imports': [
'warn',
{
patterns: [
{
group: ['@infra/*/*', '@modules/*/*', '!*.module'],
message: 'Do not deep import from a module',
},
],
},
],
},
overrides: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,8 @@ data:
}'

# Add Bettermarks' tools configuration as an external tool
# (stored in the 'external_tools' collection) that uses OAuth.
mongosh $DATABASE__URL --eval 'db.external_tools.replaceOne(
# (stored in the 'external-tools' collection) that uses OAuth.
mongosh $DATABASE__URL --eval 'db.external-tools.replaceOne(
{
"name": "bettermarks",
"config_type": "oauth2"
Expand Down Expand Up @@ -486,9 +486,9 @@ data:
echo "POSTed nextcloud to hydra."

# Add Nextcloud' tools configuration as an external tool
# (stored in the 'external_tools' collection) that uses OAuth.
echo "Inserting nextcloud to external_tools..."
mongosh $DATABASE__URL --eval 'db.external_tools.update(
# (stored in the 'external-tools' collection) that uses OAuth.
echo "Inserting nextcloud to external-tools..."
mongosh $DATABASE__URL --eval 'db.external-tools.update(
{
"name": "nextcloud",
"config_type": "oauth2"
Expand All @@ -512,7 +512,7 @@ data:
"upsert": true
}
);'
echo "Inserted nextcloud to external_tools."
echo "Inserted nextcloud to external-tools."

echo "Nextcloud config data init performed successfully."
fi
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/apps/server.app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ExpressAdapter } from '@nestjs/platform-express';
import { enableOpenApiDocs } from '@shared/controller/swagger';
import { Mail, MailService } from '@infra/mail';
import { LegacyLogger, Logger } from '@src/core/logger';
import { AccountService } from '@modules/account/services/account.service';
import { AccountService } from '@modules/account';
import { TeamService } from '@modules/teams/service/team.service';
import { AccountValidationService } from '@modules/account/services/account.validation.service';
import { AccountUc } from '@modules/account/uc/account.uc';
Expand Down
32 changes: 32 additions & 0 deletions apps/server/src/core/error/loggable/axios-error.loggable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { axiosErrorFactory } from '@shared/testing/factory';
import { AxiosError } from 'axios';
import { AxiosErrorLoggable } from './axios-error.loggable';

describe(AxiosErrorLoggable.name, () => {
describe('getLogMessage', () => {
const setup = () => {
const error = {
error: 'invalid_request',
};
const type = 'mockType';
const axiosError: AxiosError = axiosErrorFactory.withError(error).build();

const axiosErrorLoggable = new AxiosErrorLoggable(axiosError, type);

return { axiosErrorLoggable, error, axiosError };
};

it('should return error log message', () => {
const { axiosErrorLoggable, error, axiosError } = setup();

const result = axiosErrorLoggable.getLogMessage();

expect(result).toEqual({
type: 'mockType',
message: axiosError.message,
data: JSON.stringify(error),
stack: 'mockStack',
});
});
});
});
20 changes: 20 additions & 0 deletions apps/server/src/core/error/loggable/axios-error.loggable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { HttpException, HttpStatus } from '@nestjs/common';
import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger';
import { AxiosError } from 'axios';

export class AxiosErrorLoggable extends HttpException implements Loggable {
constructor(private readonly axiosError: AxiosError, protected readonly type: string) {
super(JSON.stringify(axiosError.response?.data), axiosError.status ?? HttpStatus.INTERNAL_SERVER_ERROR, {
cause: axiosError.cause,
});
}

getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage {
return {
message: this.axiosError.message,
type: this.type,
data: JSON.stringify(this.axiosError.response?.data),
stack: this.axiosError.stack,
};
}
}
2 changes: 2 additions & 0 deletions apps/server/src/core/error/loggable/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './error.loggable';
export * from './axios-error.loggable';
3 changes: 3 additions & 0 deletions apps/server/src/infra/mail/interfaces/mail-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface IMailConfig {
ADDITIONAL_BLACKLISTED_EMAIL_DOMAINS: string[];
}
3 changes: 3 additions & 0 deletions apps/server/src/infra/mail/mail.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Module, DynamicModule } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { MailService } from './mail.service';
import { IMailConfig } from './interfaces/mail-config';

interface MailModuleOptions {
exchange: string;
Expand All @@ -17,6 +19,7 @@ export class MailModule {
provide: 'MAIL_SERVICE_OPTIONS',
useValue: { exchange: options.exchange, routingKey: options.routingKey },
},
ConfigService<IMailConfig, true>,
],
exports: [MailService],
};
Expand Down
49 changes: 43 additions & 6 deletions apps/server/src/infra/mail/mail.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { Test, TestingModule } from '@nestjs/testing';
import { createMock } from '@golevelup/ts-jest';
import { ConfigService } from '@nestjs/config';
import { Mail } from './mail.interface';
import { MailService } from './mail.service';
import { IMailConfig } from './interfaces/mail-config';

describe('MailService', () => {
let module: TestingModule;
Expand All @@ -19,6 +22,10 @@ describe('MailService', () => {
MailService,
{ provide: AmqpConnection, useValue: { publish: () => {} } },
{ provide: 'MAIL_SERVICE_OPTIONS', useValue: mailServiceOptions },
{
provide: ConfigService,
useValue: createMock<ConfigService<IMailConfig, true>>({ get: () => ['schul-cloud.org', 'example.com'] }),
},
],
}).compile();

Expand All @@ -34,13 +41,43 @@ describe('MailService', () => {
expect(service).toBeDefined();
});

it('should send given data to queue', async () => {
const data: Mail = { mail: { plainTextContent: 'content', subject: 'Test' }, recipients: ['[email protected]'] };
const amqpConnectionSpy = jest.spyOn(amqpConnection, 'publish');
describe('send', () => {
describe('when recipients array is empty', () => {
it('should not send email', async () => {
const data: Mail = {
mail: { plainTextContent: 'content', subject: 'Test' },
recipients: ['[email protected]'],
};

await service.send(data);
const amqpConnectionSpy = jest.spyOn(amqpConnection, 'publish');

const expectedParams = [mailServiceOptions.exchange, mailServiceOptions.routingKey, data, { persistent: true }];
expect(amqpConnectionSpy).toHaveBeenCalledWith(...expectedParams);
await service.send(data);

expect(amqpConnectionSpy).toHaveBeenCalledTimes(0);
});
});
describe('when sending email', () => {
it('should remove email address that have blacklisted domain and send given data to queue', async () => {
const data: Mail = {
mail: { plainTextContent: 'content', subject: 'Test' },
recipients: ['[email protected]', '[email protected]', '[email protected]', '[email protected]'],
cc: ['[email protected]', '[email protected]', '[email protected]'],
bcc: ['[email protected]', '[email protected]', '[email protected]'],
replyTo: ['[email protected]', '[email protected]', '[email protected]'],
};

const amqpConnectionSpy = jest.spyOn(amqpConnection, 'publish');

await service.send(data);

expect(data.recipients).toEqual(['[email protected]']);
expect(data.cc).toEqual([]);
expect(data.bcc).toEqual(['[email protected]']);
expect(data.replyTo).toEqual(['[email protected]']);

const expectedParams = [mailServiceOptions.exchange, mailServiceOptions.routingKey, data, { persistent: true }];
expect(amqpConnectionSpy).toHaveBeenCalledWith(...expectedParams);
});
});
});
});
42 changes: 39 additions & 3 deletions apps/server/src/infra/mail/mail.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { Inject, Injectable } from '@nestjs/common';

import { ConfigService } from '@nestjs/config';
import { Mail } from './mail.interface';
import { IMailConfig } from './interfaces/mail-config';

interface MailServiceOptions {
exchange: string;
Expand All @@ -10,12 +11,47 @@ interface MailServiceOptions {

@Injectable()
export class MailService {
private readonly domainBlacklist: string[];

constructor(
private readonly amqpConnection: AmqpConnection,
@Inject('MAIL_SERVICE_OPTIONS') private readonly options: MailServiceOptions
) {}
@Inject('MAIL_SERVICE_OPTIONS') private readonly options: MailServiceOptions,
private readonly configService: ConfigService<IMailConfig, true>
) {
this.domainBlacklist = this.configService.get<string[]>('ADDITIONAL_BLACKLISTED_EMAIL_DOMAINS');
}

public async send(data: Mail): Promise<void> {
if (this.domainBlacklist.length > 0) {
data.recipients = this.filterEmailAdresses(data.recipients) as string[];
data.cc = this.filterEmailAdresses(data.cc);
data.bcc = this.filterEmailAdresses(data.bcc);
data.replyTo = this.filterEmailAdresses(data.replyTo);
}

if (data.recipients.length === 0) {
return;
}

await this.amqpConnection.publish(this.options.exchange, this.options.routingKey, data, { persistent: true });
}

private filterEmailAdresses(mails: string[] | undefined): string[] | undefined {
if (mails === undefined || mails === null) {
return mails;
}
const mailWhitelist: string[] = [];

for (const mail of mails) {
const mailDomain = this.getMailDomain(mail);
if (mailDomain && !this.domainBlacklist.includes(mailDomain)) {
mailWhitelist.push(mail);
}
}
return mailWhitelist;
}

private getMailDomain(mail: string): string {
return mail.split('@')[1];
}
}
Loading

0 comments on commit f7d65ac

Please sign in to comment.