From 3ea645b3800719e43993c57a7922457ef0660f51 Mon Sep 17 00:00:00 2001
From: HexWarrior6 <3083512469@qq.com>
Date: Fri, 14 Nov 2025 21:01:32 +0800
Subject: [PATCH 1/3] feat(email service): add email sending functionality and
related dependencies
---
backend/package-lock.json | 50 +++++++++
backend/package.json | 3 +
backend/src/app.module.ts | 2 +
backend/src/controllers/email.controller.ts | 109 ++++++++++++++++++++
backend/src/services/email.module.ts | 11 ++
backend/src/services/email.service.ts | 83 +++++++++++++++
6 files changed, 258 insertions(+)
create mode 100644 backend/src/controllers/email.controller.ts
create mode 100644 backend/src/services/email.module.ts
create mode 100644 backend/src/services/email.service.ts
diff --git a/backend/package-lock.json b/backend/package-lock.json
index e4c6116..e990dae 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -16,7 +16,10 @@
"@nestjs/mongoose": "^11.0.3",
"@nestjs/platform-express": "^11.0.1",
"bcrypt": "^6.0.0",
+ "class-transformer": "^0.5.1",
+ "class-validator": "^0.14.2",
"mongoose": "^8.18.1",
+ "nodemailer": "^7.0.10",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
@@ -2963,6 +2966,12 @@
"@types/superagent": "^8.1.0"
}
},
+ "node_modules/@types/validator": {
+ "version": "13.15.8",
+ "resolved": "https://registry.npmmirror.com/@types/validator/-/validator-13.15.8.tgz",
+ "integrity": "sha512-/NAHBJ0RwpsbLzzbLoLm/GnvCGB+A0/p5S61RUIsh7j3MP2dMkdUbWNdFqnluLlUheAs1CR2GlX2R7uzb7Tc0w==",
+ "license": "MIT"
+ },
"node_modules/@types/webidl-conversions": {
"version": "7.0.3",
"resolved": "https://registry.npmmirror.com/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
@@ -4442,6 +4451,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/class-transformer": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmmirror.com/class-transformer/-/class-transformer-0.5.1.tgz",
+ "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
+ "license": "MIT"
+ },
+ "node_modules/class-validator": {
+ "version": "0.14.2",
+ "resolved": "https://registry.npmmirror.com/class-validator/-/class-validator-0.14.2.tgz",
+ "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/validator": "^13.11.8",
+ "libphonenumber-js": "^1.11.1",
+ "validator": "^13.9.0"
+ }
+ },
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz",
@@ -7423,6 +7449,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/libphonenumber-js": {
+ "version": "1.12.26",
+ "resolved": "https://registry.npmmirror.com/libphonenumber-js/-/libphonenumber-js-1.12.26.tgz",
+ "integrity": "sha512-MagMOuqEXB2Pa90cWE+BoCmcKJx+de5uBIicaUkQ+uiEslZ0OBMNOkSZT/36syXNHu68UeayTxPm3DYM2IHoLQ==",
+ "license": "MIT"
+ },
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -8064,6 +8096,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/nodemailer": {
+ "version": "7.0.10",
+ "resolved": "https://registry.npmmirror.com/nodemailer/-/nodemailer-7.0.10.tgz",
+ "integrity": "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==",
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -10208,6 +10249,15 @@
"node": ">=10.12.0"
}
},
+ "node_modules/validator": {
+ "version": "13.15.23",
+ "resolved": "https://registry.npmmirror.com/validator/-/validator-13.15.23.tgz",
+ "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
diff --git a/backend/package.json b/backend/package.json
index 731c9c5..c0ef817 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -27,7 +27,10 @@
"@nestjs/mongoose": "^11.0.3",
"@nestjs/platform-express": "^11.0.1",
"bcrypt": "^6.0.0",
+ "class-transformer": "^0.5.1",
+ "class-validator": "^0.14.2",
"mongoose": "^8.18.1",
+ "nodemailer": "^7.0.10",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts
index d0bd6e4..f1f0016 100644
--- a/backend/src/app.module.ts
+++ b/backend/src/app.module.ts
@@ -5,6 +5,7 @@ import { MongooseModule } from '@nestjs/mongoose';
import { ConfigModule } from '@nestjs/config';
import { ArticleModule } from './api/article/article.module';
import { UserModule } from './api/user/user.module';
+import { EmailModule } from './services/email.module';
@Module({
imports: [
@@ -12,6 +13,7 @@ import { UserModule } from './api/user/user.module';
MongooseModule.forRoot(process.env.DB_URI || ''),
ArticleModule,
UserModule,
+ EmailModule,
],
controllers: [AppController],
providers: [AppService],
diff --git a/backend/src/controllers/email.controller.ts b/backend/src/controllers/email.controller.ts
new file mode 100644
index 0000000..521c873
--- /dev/null
+++ b/backend/src/controllers/email.controller.ts
@@ -0,0 +1,109 @@
+import { Controller, Post, Get, Body, Query, UsePipes, ValidationPipe } from '@nestjs/common';
+import { EmailService } from '../services/email.service';
+import { IsEmail, IsString, IsArray, ValidateNested, IsOptional } from 'class-validator';
+import { Type } from 'class-transformer';
+
+class SendEmailDto {
+ @IsEmail()
+ to: string;
+
+ @IsString()
+ subject: string;
+
+ @IsString()
+ html: string;
+
+ @IsString()
+ @IsOptional()
+ text?: string;
+
+ @IsString()
+ @IsOptional()
+ fromName?: string;
+}
+
+class SendBulkEmailDto {
+ @IsArray()
+ @IsEmail({}, { each: true })
+ to: string[];
+
+ @IsString()
+ subject: string;
+
+ @IsString()
+ html: string;
+
+ @IsString()
+ @IsOptional()
+ text?: string;
+
+ @IsString()
+ @IsOptional()
+ fromName?: string;
+}
+
+@Controller('email')
+export class EmailController {
+ constructor(private readonly emailService: EmailService) {}
+
+ @Post('send')
+ @UsePipes(new ValidationPipe({ transform: true }))
+ async sendEmail(@Body() sendEmailDto: SendEmailDto) {
+ const { to, subject, html, text, fromName } = sendEmailDto;
+ const result = await this.emailService.sendMail(to, subject, html, text, fromName);
+
+ if (result) {
+ return {
+ success: true,
+ message: '邮件发送成功',
+ };
+ } else {
+ return {
+ success: false,
+ message: '邮件发送失败',
+ };
+ }
+ }
+
+ @Post('send-bulk')
+ @UsePipes(new ValidationPipe({ transform: true }))
+ async sendBulkEmail(@Body() sendBulkEmailDto: SendBulkEmailDto) {
+ const { to, subject, html, text, fromName } = sendBulkEmailDto;
+ const result = await this.emailService.sendBulkMail(to, subject, html, text, fromName);
+
+ if (result) {
+ return {
+ success: true,
+ message: '批量邮件发送成功',
+ };
+ } else {
+ return {
+ success: false,
+ message: '批量邮件发送失败',
+ };
+ }
+ }
+
+ @Get('test')
+ async testEmail(@Query('to') to: string, @Query('fromName') fromName?: string) {
+ const result = await this.emailService.sendMail(
+ to,
+ '测试邮件',
+ '
这是一封测试邮件
邮件发送功能正常工作。
',
+ '这是一封测试邮件,邮件发送功能正常工作。',
+ fromName,
+ );
+
+ if (result) {
+ return {
+ success: true,
+ message: '测试邮件发送成功',
+ };
+ } else {
+ return {
+ success: false,
+ message: '测试邮件发送失败',
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/backend/src/services/email.module.ts b/backend/src/services/email.module.ts
new file mode 100644
index 0000000..a7f73ae
--- /dev/null
+++ b/backend/src/services/email.module.ts
@@ -0,0 +1,11 @@
+import { Module } from '@nestjs/common';
+import { EmailService } from './email.service';
+import { EmailController } from '../controllers/email.controller';
+import { ConfigService } from '@nestjs/config';
+
+@Module({
+ providers: [EmailService, ConfigService],
+ controllers: [EmailController],
+ exports: [EmailService],
+})
+export class EmailModule {}
\ No newline at end of file
diff --git a/backend/src/services/email.service.ts b/backend/src/services/email.service.ts
new file mode 100644
index 0000000..e8b4025
--- /dev/null
+++ b/backend/src/services/email.service.ts
@@ -0,0 +1,83 @@
+import { Injectable } from '@nestjs/common';
+import * as nodemailer from 'nodemailer';
+import { ConfigService } from '@nestjs/config';
+
+@Injectable()
+export class EmailService {
+ private transporter: nodemailer.Transporter;
+
+ constructor(private configService: ConfigService) {
+ // 从配置中获取邮箱相关设置
+ const emailHost = this.configService.get('EMAIL_HOST');
+ const emailPort = this.configService.get('EMAIL_PORT');
+ const emailUser = this.configService.get('EMAIL_USER');
+ const emailPassword = this.configService.get('EMAIL_PASSWORD');
+ const emailSecure = this.configService.get('EMAIL_SECURE') === 'true';
+
+ this.transporter = nodemailer.createTransport({
+ host: emailHost,
+ port: emailPort,
+ secure: emailSecure, // true for 465, false for other ports
+ auth: {
+ user: emailUser, // 邮箱地址
+ pass: emailPassword, // 邮箱授权码
+ },
+ });
+ }
+
+ async sendMail(
+ to: string,
+ subject: string,
+ html: string,
+ text?: string,
+ fromName?: string,
+ ): Promise {
+ try {
+ const emailUser = this.configService.get('EMAIL_USER');
+ const from = fromName ? `"${fromName}" <${emailUser}>` : emailUser;
+
+ const mailOptions = {
+ from, // 发件人邮箱,包含可选的发件人名称
+ to, // 收件人邮箱
+ subject, // 邮件主题
+ text, // 纯文本内容(可选)
+ html, // HTML 内容
+ };
+
+ const info = await this.transporter.sendMail(mailOptions);
+ console.log('邮件发送成功:', info.messageId);
+ return true;
+ } catch (error) {
+ console.error('邮件发送失败:', error);
+ return false;
+ }
+ }
+
+ async sendBulkMail(
+ to: string[],
+ subject: string,
+ html: string,
+ text?: string,
+ fromName?: string,
+ ): Promise {
+ try {
+ const emailUser = this.configService.get('EMAIL_USER');
+ const from = fromName ? `"${fromName}" <${emailUser}>` : emailUser;
+
+ const mailOptions = {
+ from, // 发件人邮箱,包含可选的发件人名称
+ to: to.join(', '), // 多个收件人
+ subject, // 邮件主题
+ text, // 纯文本内容(可选)
+ html, // HTML 内容
+ };
+
+ const info = await this.transporter.sendMail(mailOptions);
+ console.log('批量邮件发送成功:', info.messageId);
+ return true;
+ } catch (error) {
+ console.error('批量邮件发送失败:', error);
+ return false;
+ }
+ }
+}
\ No newline at end of file
From bf7349fed0876ed01949f5ac257519b5e3e1395f Mon Sep 17 00:00:00 2001
From: HexWarrior6 <3083512469@qq.com>
Date: Fri, 14 Nov 2025 21:56:23 +0800
Subject: [PATCH 2/3] feat(article): add the email notification function for
article review status
---
backend/src/api/article/article.module.ts | 5 +-
backend/src/api/article/article.service.ts | 101 +++++++++++++++++++-
backend/src/controllers/email.controller.ts | 18 ++--
backend/src/services/email.service.ts | 34 +++----
4 files changed, 129 insertions(+), 29 deletions(-)
diff --git a/backend/src/api/article/article.module.ts b/backend/src/api/article/article.module.ts
index 17a0268..629097d 100644
--- a/backend/src/api/article/article.module.ts
+++ b/backend/src/api/article/article.module.ts
@@ -1,15 +1,18 @@
// article.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
+import { ConfigModule } from '@nestjs/config';
import { ArticleController } from './article.controller';
import { ArticleService } from './article.service';
import { Article, ArticleSchema } from './article.schema';
+import { EmailService } from '../../services/email.service';
@Module({
imports: [
MongooseModule.forFeature([{ name: Article.name, schema: ArticleSchema }]),
+ ConfigModule,
],
controllers: [ArticleController],
- providers: [ArticleService],
+ providers: [ArticleService, EmailService],
})
export class ArticleModule {}
diff --git a/backend/src/api/article/article.service.ts b/backend/src/api/article/article.service.ts
index 55c16b2..2eab37a 100644
--- a/backend/src/api/article/article.service.ts
+++ b/backend/src/api/article/article.service.ts
@@ -4,11 +4,13 @@ import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Article, ArticleDocument, ArticleStatus } from './article.schema';
import { CreateArticleDto, ReviewArticleDto } from './create-article.dto';
+import { EmailService } from '../../services/email.service';
@Injectable()
export class ArticleService {
constructor(
@InjectModel(Article.name) private articleModel: Model,
+ private emailService: EmailService,
) {}
// Get all articles (with optional filtering)
@@ -39,7 +41,10 @@ export class ArticleService {
}
// Find articles with similar DOIs (for duplicate checking)
- async findArticlesBySimilarDOI(doi: string, excludeId?: string): Promise {
+ async findArticlesBySimilarDOI(
+ doi: string,
+ excludeId?: string,
+ ): Promise {
// Query object to build the search criteria
const query: any = { doi };
@@ -175,7 +180,10 @@ export class ArticleService {
updateData['isDuplicate'] = true;
// Prevent an article from being marked as duplicate of itself
if (reviewData.duplicateOf === id) {
- throw new HttpException('Cannot mark article as duplicate of itself', HttpStatus.BAD_REQUEST);
+ throw new HttpException(
+ 'Cannot mark article as duplicate of itself',
+ HttpStatus.BAD_REQUEST,
+ );
}
updateData['duplicateOf'] = reviewData.duplicateOf;
} else {
@@ -195,9 +203,98 @@ export class ArticleService {
);
}
+ // Send email notification to submitter
+ console.log(`Attempting to send notification email for article ${updatedArticle.customId} with status ${updatedArticle.status}`);
+ await this.sendReviewNotificationEmail(updatedArticle);
+
return updatedArticle;
}
+ // Send email notification to submitter about article review status
+ private async sendReviewNotificationEmail(article: Article) {
+ console.log(`Processing notification for article ${article.customId} with status ${article.status} and submitter email ${article.submitterEmail}`);
+
+ if (!article.submitterEmail) {
+ console.warn(`No submitter email found for article ${article.customId}`);
+ return;
+ }
+
+ let subject = '';
+ let htmlContent = '';
+
+ // Determine the content based on the status
+ if (article.status === ArticleStatus.APPROVED) {
+ subject = 'Your Article Has Been Approved - CISE_SPEED';
+ htmlContent = `
+ Article Review Result Notification
+ Hello,
+ Your article "${article.title}" has been approved and added to the database.
+ Article Details:
+
+ - Title: ${article.title}
+ - ID: ${article.customId}
+ - Authors: ${article.authors}
+ - Publication Year: ${article.pubyear}
+ - Evidence Type: ${article.evidence}
+
+ Thank you for contributing to the CISE_SPEED database!
+ If you have any questions, please feel free to contact us.
+ `;
+ } else if (article.status === ArticleStatus.REJECTED) {
+ subject = 'Article Review Result - CISE_SPEED';
+ htmlContent = `
+ Article Review Result Notification
+ Hello,
+ Unfortunately, your article "${article.title}" did not pass the review.
+
+ ${article.reviewComment ? `Review Comment: ${article.reviewComment}
` : ''}
+
+ Article Details:
+
+ - Title: ${article.title}
+ - ID: ${article.customId}
+ - Authors: ${article.authors}
+ - Publication Year: ${article.pubyear}
+
+ Thank you for your interest and contribution to the CISE_SPEED database!
+ If you have any questions, please feel free to contact us.
+ `;
+ } else {
+ // For other status changes (like back to PENDING), we could send a generic notification
+ console.log(
+ `Article ${article.customId} status changed to ${article.status}, no email notification required.`,
+ );
+ return;
+ }
+
+ console.log(`Attempting to send email to ${article.submitterEmail} with subject: ${subject}`);
+
+ try {
+ const result = await this.emailService.sendMail(
+ article.submitterEmail,
+ subject,
+ htmlContent,
+ `CISE_SPEED: Your article "${article.title}" has been reviewed`, // text
+ 'CISE_SPEED System', // fromName
+ );
+
+ if (result) {
+ console.log(
+ `Notification email sent successfully to ${article.submitterEmail} for article ${article.customId}`,
+ );
+ } else {
+ console.error(
+ `Failed to send notification email to ${article.submitterEmail} for article ${article.customId}`,
+ );
+ }
+ } catch (error) {
+ console.error(
+ `Error sending notification email to ${article.submitterEmail} for article ${article.customId}:`,
+ error,
+ );
+ }
+ }
+
// Search articles by keywords and filters with sorting
async searchArticles(
keywords: string,
diff --git a/backend/src/controllers/email.controller.ts b/backend/src/controllers/email.controller.ts
index 521c873..91c36db 100644
--- a/backend/src/controllers/email.controller.ts
+++ b/backend/src/controllers/email.controller.ts
@@ -55,12 +55,12 @@ export class EmailController {
if (result) {
return {
success: true,
- message: '邮件发送成功',
+ message: 'Email sent successfully',
};
} else {
return {
success: false,
- message: '邮件发送失败',
+ message: 'Email sending failed',
};
}
}
@@ -74,12 +74,12 @@ export class EmailController {
if (result) {
return {
success: true,
- message: '批量邮件发送成功',
+ message: 'Bulk email sent successfully',
};
} else {
return {
success: false,
- message: '批量邮件发送失败',
+ message: 'Bulk email sending failed',
};
}
}
@@ -88,21 +88,21 @@ export class EmailController {
async testEmail(@Query('to') to: string, @Query('fromName') fromName?: string) {
const result = await this.emailService.sendMail(
to,
- '测试邮件',
- '这是一封测试邮件
邮件发送功能正常工作。
',
- '这是一封测试邮件,邮件发送功能正常工作。',
+ 'Test Email',
+ 'This is a test email
Email sending functionality is working properly.
',
+ 'This is a test email, email sending functionality is working properly.',
fromName,
);
if (result) {
return {
success: true,
- message: '测试邮件发送成功',
+ message: 'Test email sent successfully',
};
} else {
return {
success: false,
- message: '测试邮件发送失败',
+ message: 'Test email sending failed',
};
}
}
diff --git a/backend/src/services/email.service.ts b/backend/src/services/email.service.ts
index e8b4025..5e54891 100644
--- a/backend/src/services/email.service.ts
+++ b/backend/src/services/email.service.ts
@@ -7,7 +7,7 @@ export class EmailService {
private transporter: nodemailer.Transporter;
constructor(private configService: ConfigService) {
- // 从配置中获取邮箱相关设置
+ // Get email configuration from environment
const emailHost = this.configService.get('EMAIL_HOST');
const emailPort = this.configService.get('EMAIL_PORT');
const emailUser = this.configService.get('EMAIL_USER');
@@ -19,8 +19,8 @@ export class EmailService {
port: emailPort,
secure: emailSecure, // true for 465, false for other ports
auth: {
- user: emailUser, // 邮箱地址
- pass: emailPassword, // 邮箱授权码
+ user: emailUser, // email address
+ pass: emailPassword, // email password/authorization code
},
});
}
@@ -37,18 +37,18 @@ export class EmailService {
const from = fromName ? `"${fromName}" <${emailUser}>` : emailUser;
const mailOptions = {
- from, // 发件人邮箱,包含可选的发件人名称
- to, // 收件人邮箱
- subject, // 邮件主题
- text, // 纯文本内容(可选)
- html, // HTML 内容
+ from, // sender email with optional name
+ to, // recipient email
+ subject, // email subject
+ text, // plain text content (optional)
+ html, // HTML content
};
const info = await this.transporter.sendMail(mailOptions);
- console.log('邮件发送成功:', info.messageId);
+ console.log('Email sent successfully:', info.messageId);
return true;
} catch (error) {
- console.error('邮件发送失败:', error);
+ console.error('Email sending failed:', error);
return false;
}
}
@@ -65,18 +65,18 @@ export class EmailService {
const from = fromName ? `"${fromName}" <${emailUser}>` : emailUser;
const mailOptions = {
- from, // 发件人邮箱,包含可选的发件人名称
- to: to.join(', '), // 多个收件人
- subject, // 邮件主题
- text, // 纯文本内容(可选)
- html, // HTML 内容
+ from, // sender email with optional name
+ to: to.join(', '), // multiple recipients
+ subject, // email subject
+ text, // plain text content (optional)
+ html, // HTML content
};
const info = await this.transporter.sendMail(mailOptions);
- console.log('批量邮件发送成功:', info.messageId);
+ console.log('Bulk email sent successfully:', info.messageId);
return true;
} catch (error) {
- console.error('批量邮件发送失败:', error);
+ console.error('Bulk email sending failed:', error);
return false;
}
}
From e30a3b090e8faf6ab291acfdc628ad1e2f8d5d45 Mon Sep 17 00:00:00 2001
From: HexWarrior6 <3083512469@qq.com>
Date: Fri, 14 Nov 2025 22:01:03 +0800
Subject: [PATCH 3/3] fix: add EmailService dependency to ArticleService tests
---
.../src/api/article/article.service.spec.ts | 21 +++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/backend/src/api/article/article.service.spec.ts b/backend/src/api/article/article.service.spec.ts
index 094ad9c..28e9042 100644
--- a/backend/src/api/article/article.service.spec.ts
+++ b/backend/src/api/article/article.service.spec.ts
@@ -3,6 +3,8 @@ import { getModelToken } from '@nestjs/mongoose';
import { ArticleService } from './article.service';
import { Article, ArticleStatus, EvidenceType } from './article.schema';
import { Model } from 'mongoose';
+import { EmailService } from '../../services/email.service';
+import { ConfigService } from '@nestjs/config';
// Mock Article model
const mockArticleModel = {
@@ -16,6 +18,17 @@ const mockArticleModel = {
exec: jest.fn(),
};
+// Mock EmailService
+const mockEmailService = {
+ sendMail: jest.fn().mockResolvedValue(true),
+ sendBulkMail: jest.fn().mockResolvedValue(true),
+};
+
+// Mock ConfigService
+const mockConfigService = {
+ get: jest.fn(),
+};
+
describe('ArticleService', () => {
let service: ArticleService;
let model: Model;
@@ -28,6 +41,14 @@ describe('ArticleService', () => {
provide: getModelToken(Article.name),
useValue: mockArticleModel,
},
+ {
+ provide: EmailService,
+ useValue: mockEmailService,
+ },
+ {
+ provide: ConfigService,
+ useValue: mockConfigService,
+ },
],
}).compile();