feat: 完善单测 (#71)
This commit is contained in:
parent
292082e3b0
commit
8d29865ff0
@ -1,6 +1,6 @@
|
||||
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
|
||||
XIAOJU_SURVEY_MONGO_URL=mongodb://localhost:27017
|
||||
XIAOJU_SURVEY_MONGO_AUTH_SOURCE=
|
||||
XIAOJU_SURVEY_MONGO_AUTH_SOURCE=admin
|
||||
|
||||
|
||||
XIAOJU_SURVEY_RESPONSE_AES_ENCRYPT_SECRET_KEY=dataAesEncryptSecretKey
|
||||
|
@ -5,11 +5,11 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>问卷管理端</title>
|
||||
<link rel="stylesheet" href="./commom.css">
|
||||
<link rel="stylesheet" href="/commom.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="main">
|
||||
<img src="./nodata.png" alt="">
|
||||
<img src="/nodata.png" alt="">
|
||||
<p class="title">暂无数据</p>
|
||||
</div>
|
||||
</body>
|
||||
|
@ -5,11 +5,11 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>问卷管理端</title>
|
||||
<link rel="stylesheet" href="./commom.css">
|
||||
<link rel="stylesheet" href="/commom.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="main">
|
||||
<img src="./nodata.png" alt="">
|
||||
<img src="/nodata.png" alt="">
|
||||
<p class="title">暂无数据</p>
|
||||
</div>
|
||||
</body>
|
||||
|
@ -5,11 +5,11 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>问卷投放端</title>
|
||||
<link rel="stylesheet" href="./commom.css">
|
||||
<link rel="stylesheet" href="/commom.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="main">
|
||||
<img src="./nodata.png" alt="">
|
||||
<img src="/nodata.png" alt="">
|
||||
<p class="title">暂无数据</p>
|
||||
</div>
|
||||
</body>
|
||||
|
18
server/src/app.controller.spec.ts
Normal file
18
server/src/app.controller.spec.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
|
||||
describe('AppController', () => {
|
||||
let controller: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
@ -46,7 +46,7 @@ import { Logger } from './logger';
|
||||
const authSource =
|
||||
(await configService.get<string>(
|
||||
'XIAOJU_SURVEY_MONGO_AUTH_SOURCE',
|
||||
)) || '';
|
||||
)) || 'admin';
|
||||
const database = await configService.get<string>(
|
||||
'XIAOJU_SURVEY_MONGO_DB_NAME',
|
||||
);
|
||||
@ -94,7 +94,6 @@ export class AppModule {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly pluginManager: XiaojuSurveyPluginManager,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(LogRequestMiddleware).forRoutes('*');
|
||||
@ -108,7 +107,7 @@ export class AppModule {
|
||||
),
|
||||
new SurveyUtilPlugin(),
|
||||
);
|
||||
this.logger.init({
|
||||
Logger.init({
|
||||
filename: this.configService.get<string>('XIAOJU_SURVEY_LOGGER_FILENAME'),
|
||||
});
|
||||
}
|
||||
|
126
server/src/guards/authtication.spec.ts
Normal file
126
server/src/guards/authtication.spec.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Authtication } from './authtication';
|
||||
import { UserService } from '../modules/auth/services/user.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AuthtificationException } from '../exceptions/authException';
|
||||
import { User } from 'src/models/user.entity';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
|
||||
jest.mock('jsonwebtoken');
|
||||
|
||||
describe('Authtication', () => {
|
||||
let guard: Authtication;
|
||||
let userService: UserService;
|
||||
let configService: ConfigService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
Authtication,
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
getUserByUsername: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
guard = module.get<Authtication>(Authtication);
|
||||
userService = module.get<UserService>(UserService);
|
||||
configService = module.get<ConfigService>(ConfigService);
|
||||
});
|
||||
|
||||
it('should throw exception if token is not provided', async () => {
|
||||
const context = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
headers: {},
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
await expect(guard.canActivate(context as any)).rejects.toThrow(
|
||||
AuthtificationException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw exception if token is invalid', async () => {
|
||||
const context = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
headers: {
|
||||
authorization: 'Bearer invalidToken',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
jest.spyOn(jwt, 'verify').mockReturnValue(new Error('token is invalid'));
|
||||
|
||||
jest
|
||||
.spyOn(configService, 'get')
|
||||
.mockReturnValue('XIAOJU_SURVEY_JWT_SECRET');
|
||||
|
||||
await expect(guard.canActivate(context as any)).rejects.toThrow(
|
||||
AuthtificationException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw exception if user does not exist', async () => {
|
||||
const context = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
headers: {
|
||||
authorization: 'Bearer validToken',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
const fakeUser = { username: 'testUser' } as User;
|
||||
|
||||
jest.spyOn(jwt, 'verify').mockReturnValue(fakeUser);
|
||||
|
||||
jest
|
||||
.spyOn(configService, 'get')
|
||||
.mockReturnValue('XIAOJU_SURVEY_JWT_SECRET');
|
||||
jest.spyOn(userService, 'getUserByUsername').mockResolvedValue(null);
|
||||
|
||||
await expect(guard.canActivate(context as any)).rejects.toThrow(
|
||||
AuthtificationException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should set user in request object and return true if user exists', async () => {
|
||||
const request = {
|
||||
headers: {
|
||||
authorization: 'Bearer validToken',
|
||||
},
|
||||
};
|
||||
const context = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => request,
|
||||
}),
|
||||
};
|
||||
|
||||
const fakeUser = { username: 'testUser' } as User;
|
||||
jest
|
||||
.spyOn(configService, 'get')
|
||||
.mockReturnValue('XIAOJU_SURVEY_JWT_SECRET');
|
||||
jest.spyOn(userService, 'getUserByUsername').mockResolvedValue(fakeUser);
|
||||
|
||||
jest.spyOn(jwt, 'verify').mockReturnValue(fakeUser);
|
||||
|
||||
const result = await guard.canActivate(context as any);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(request['user']).toEqual(fakeUser);
|
||||
});
|
||||
});
|
@ -39,35 +39,34 @@ export interface DataItem {
|
||||
showType: boolean;
|
||||
showSpliter: boolean;
|
||||
type: string;
|
||||
valid: string;
|
||||
valid?: string;
|
||||
field: string;
|
||||
title: string;
|
||||
placeholder: string;
|
||||
randomSort: boolean;
|
||||
randomSort?: boolean;
|
||||
checked: boolean;
|
||||
minNum: string;
|
||||
maxNum: string;
|
||||
star: number;
|
||||
nps: NPS;
|
||||
nps?: NPS;
|
||||
placeholderDesc: string;
|
||||
addressType: number;
|
||||
isAuto: boolean;
|
||||
urlKey: string;
|
||||
textRange: TextRange;
|
||||
textRange?: TextRange;
|
||||
options?: Option[];
|
||||
importKey?: string;
|
||||
importData?: string;
|
||||
cOption?: string;
|
||||
cOptions?: string[];
|
||||
exclude?: boolean;
|
||||
rangeConfig?: any;
|
||||
starStyle?: string;
|
||||
innerType?: string;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
text: string;
|
||||
imageUrl: string;
|
||||
others: boolean;
|
||||
mustOthers: boolean;
|
||||
othersKey: string;
|
||||
mustOthers?: boolean;
|
||||
othersKey?: string;
|
||||
placeholderDesc: string;
|
||||
hash: string;
|
||||
}
|
||||
@ -109,10 +108,16 @@ export interface SkinConf {
|
||||
inputBgColor: string;
|
||||
}
|
||||
|
||||
export interface BottomConf {
|
||||
logoImage: string;
|
||||
logoImageWidth: string;
|
||||
}
|
||||
|
||||
export interface SurveySchemaInterface {
|
||||
bannerConf: BannerConf;
|
||||
dataConf: DataConf;
|
||||
submitConf: SubmitConf;
|
||||
baseConf: BaseConf;
|
||||
skinConf: SkinConf;
|
||||
bottomConf: BottomConf;
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
import * as log4js from 'log4js';
|
||||
import moment from 'moment';
|
||||
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { Inject, Request } from '@nestjs/common';
|
||||
const log4jsLogger = log4js.getLogger();
|
||||
|
||||
export class Logger {
|
||||
private traceId: string = '';
|
||||
private inited = false;
|
||||
private static inited = false;
|
||||
|
||||
init(config: { filename: string }) {
|
||||
constructor(@Inject(REQUEST) private req: Request) {}
|
||||
|
||||
static init(config: { filename: string }) {
|
||||
if (this.inited) {
|
||||
return;
|
||||
}
|
||||
@ -31,17 +33,15 @@ export class Logger {
|
||||
});
|
||||
}
|
||||
|
||||
setTraceId(traceId: string) {
|
||||
this.traceId = traceId;
|
||||
}
|
||||
|
||||
_log(message, options: { dltag?: string; level: string }) {
|
||||
const datetime = moment().format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||
const level = options.level;
|
||||
const dltag = options.dltag ? `${options.dltag}||` : '';
|
||||
const traceId = this.traceId ? `traceid=${this.traceId}||` : '';
|
||||
const traceIdStr = this.req['traceId']
|
||||
? `traceid=${this.req['traceId']}||`
|
||||
: '';
|
||||
return log4jsLogger[level](
|
||||
`[${datetime}][${level.toUpperCase()}]${dltag}${traceId}${message}`,
|
||||
`[${datetime}][${level.toUpperCase()}]${dltag}${traceIdStr}${message}`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -53,5 +53,3 @@ export class Logger {
|
||||
return this._log(message, { ...options, level: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
export default new Logger();
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
|
||||
import logger, { Logger } from './index';
|
||||
import { Logger } from './index';
|
||||
|
||||
export const LoggerProvider: Provider = {
|
||||
provide: Logger,
|
||||
useValue: logger,
|
||||
useClass: Logger,
|
||||
};
|
||||
|
@ -13,7 +13,7 @@ export class LogRequestMiddleware implements NestMiddleware {
|
||||
const userAgent = req.get('user-agent') || '';
|
||||
const startTime = Date.now();
|
||||
const traceId = genTraceId({ ip });
|
||||
this.logger.setTraceId(traceId);
|
||||
req['traceId'] = traceId;
|
||||
const query = JSON.stringify(req.query);
|
||||
const body = JSON.stringify(req.body);
|
||||
this.logger.info(
|
||||
|
30
server/src/models/__test/base.entity.spec.ts
Normal file
30
server/src/models/__test/base.entity.spec.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { BaseEntity } from '../base.entity';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
|
||||
describe('BaseEntity', () => {
|
||||
let baseEntity: BaseEntity;
|
||||
|
||||
beforeEach(() => {
|
||||
baseEntity = new BaseEntity();
|
||||
});
|
||||
|
||||
it('should initialize default info before insert', () => {
|
||||
const now = Date.now();
|
||||
baseEntity.initDefaultInfo();
|
||||
|
||||
expect(baseEntity.curStatus.status).toBe(RECORD_STATUS.NEW);
|
||||
expect(baseEntity.curStatus.date).toBeCloseTo(now, -3);
|
||||
expect(baseEntity.statusList).toHaveLength(1);
|
||||
expect(baseEntity.statusList[0].status).toBe(RECORD_STATUS.NEW);
|
||||
expect(baseEntity.statusList[0].date).toBeCloseTo(now, -3);
|
||||
expect(baseEntity.createDate).toBeCloseTo(now, -3);
|
||||
expect(baseEntity.updateDate).toBeCloseTo(now, -3);
|
||||
});
|
||||
|
||||
it('should update updateDate before update', () => {
|
||||
const now = Date.now();
|
||||
baseEntity.onUpdate();
|
||||
|
||||
expect(baseEntity.updateDate).toBeCloseTo(now, -3); // Check if date is close to current time
|
||||
});
|
||||
});
|
42
server/src/models/__test/surveyResponse.entity.spec.ts
Normal file
42
server/src/models/__test/surveyResponse.entity.spec.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { SurveyResponse } from '../surveyResponse.entity';
|
||||
import pluginManager from 'src/securityPlugin/pluginManager';
|
||||
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
const mockOriginData = {
|
||||
data405: '浙江省杭州市西湖区xxx',
|
||||
data450: '450111000000000000',
|
||||
data458: '15000000000',
|
||||
data515: '115019',
|
||||
data770: '123456@qq.com',
|
||||
};
|
||||
|
||||
describe('SurveyResponse', () => {
|
||||
beforeEach(() => {
|
||||
pluginManager.registerPlugin(
|
||||
new ResponseSecurityPlugin('dataAesEncryptSecretKey'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should encrypt and decrypt success', async () => {
|
||||
const surveyResponse = new SurveyResponse();
|
||||
surveyResponse.data = cloneDeep(mockOriginData);
|
||||
await surveyResponse.onDataInsert();
|
||||
expect(surveyResponse.data.data405).not.toBe(mockOriginData.data405);
|
||||
expect(surveyResponse.data.data450).not.toBe(mockOriginData.data450);
|
||||
expect(surveyResponse.data.data458).not.toBe(mockOriginData.data458);
|
||||
expect(surveyResponse.data.data770).not.toBe(mockOriginData.data770);
|
||||
expect(surveyResponse.secretKeys).toEqual([
|
||||
'data405',
|
||||
'data450',
|
||||
'data458',
|
||||
'data770',
|
||||
]);
|
||||
|
||||
surveyResponse.onDataLoaded();
|
||||
expect(surveyResponse.data.data405).toBe(mockOriginData.data405);
|
||||
expect(surveyResponse.data.data450).toBe(mockOriginData.data450);
|
||||
expect(surveyResponse.data.data458).toBe(mockOriginData.data458);
|
||||
expect(surveyResponse.data.data770).toBe(mockOriginData.data770);
|
||||
});
|
||||
});
|
43
server/src/models/base.entity.ts
Normal file
43
server/src/models/base.entity.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Column, ObjectIdColumn, BeforeInsert, BeforeUpdate } from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
|
||||
export class BaseEntity {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
@BeforeInsert()
|
||||
initDefaultInfo() {
|
||||
const now = Date.now();
|
||||
if (!this.curStatus) {
|
||||
const curStatus = { status: RECORD_STATUS.NEW, date: now };
|
||||
this.curStatus = curStatus;
|
||||
this.statusList = [curStatus];
|
||||
}
|
||||
this.createDate = now;
|
||||
this.updateDate = now;
|
||||
}
|
||||
|
||||
@BeforeUpdate()
|
||||
onUpdate() {
|
||||
this.updateDate = Date.now();
|
||||
}
|
||||
}
|
@ -1,16 +1,9 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
Index,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { Entity, Column, Index, ObjectIdColumn } from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
import { BaseEntity } from './base.entity';
|
||||
|
||||
@Entity({ name: 'captcha' })
|
||||
export class Captcha {
|
||||
export class Captcha extends BaseEntity {
|
||||
@Index({
|
||||
expireAfterSeconds:
|
||||
new Date(Date.now() + 2 * 60 * 60 * 1000).getTime() / 1000,
|
||||
@ -18,41 +11,6 @@ export class Captcha {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
@Column()
|
||||
text: string;
|
||||
|
||||
@BeforeInsert()
|
||||
initDefaultInfo() {
|
||||
const now = Date.now();
|
||||
if (!this.curStatus) {
|
||||
const curStatus = { status: RECORD_STATUS.NEW, date: now };
|
||||
this.curStatus = curStatus;
|
||||
this.statusList = [curStatus];
|
||||
}
|
||||
this.createDate = now;
|
||||
this.updateDate = now;
|
||||
}
|
||||
|
||||
@BeforeUpdate()
|
||||
onUpdate() {
|
||||
this.updateDate = Date.now();
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,10 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
Index,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { Entity, Column, Index, ObjectIdColumn } from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
import { ENCRYPT_TYPE } from '../enums/encrypt';
|
||||
import { BaseEntity } from './base.entity';
|
||||
|
||||
@Entity({ name: 'clientEncrypt' })
|
||||
export class ClientEncrypt {
|
||||
export class ClientEncrypt extends BaseEntity {
|
||||
@Index({
|
||||
expireAfterSeconds:
|
||||
new Date(Date.now() + 2 * 60 * 60 * 1000).getTime() / 1000,
|
||||
@ -19,18 +12,6 @@ export class ClientEncrypt {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column('jsonb')
|
||||
data: {
|
||||
secretKey?: string; // aes加密的密钥
|
||||
@ -40,27 +21,4 @@ export class ClientEncrypt {
|
||||
|
||||
@Column()
|
||||
type: ENCRYPT_TYPE;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
@BeforeInsert()
|
||||
initDefaultInfo() {
|
||||
const now = Date.now();
|
||||
if (!this.curStatus) {
|
||||
const curStatus = { status: RECORD_STATUS.NEW, date: now };
|
||||
this.curStatus = curStatus;
|
||||
this.statusList = [curStatus];
|
||||
}
|
||||
this.createDate = now;
|
||||
this.updateDate = now;
|
||||
}
|
||||
|
||||
@BeforeUpdate()
|
||||
onUpdate() {
|
||||
this.updateDate = Date.now();
|
||||
}
|
||||
}
|
||||
|
@ -1,36 +1,8 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { BaseEntity } from './base.entity';
|
||||
|
||||
@Entity({ name: 'counter' })
|
||||
export class Counter {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
export class Counter extends BaseEntity {
|
||||
@Column()
|
||||
key: string;
|
||||
|
||||
@ -42,21 +14,4 @@ export class Counter {
|
||||
|
||||
@Column('jsonb')
|
||||
data: Record<string, any>;
|
||||
|
||||
@BeforeInsert()
|
||||
initDefaultInfo() {
|
||||
const now = Date.now();
|
||||
if (!this.curStatus) {
|
||||
const curStatus = { status: RECORD_STATUS.NEW, date: now };
|
||||
this.curStatus = curStatus;
|
||||
this.statusList = [curStatus];
|
||||
}
|
||||
this.createDate = now;
|
||||
this.updateDate = now;
|
||||
}
|
||||
|
||||
@BeforeUpdate()
|
||||
onUpdate() {
|
||||
this.updateDate = Date.now();
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +1,9 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { SurveySchemaInterface } from '../interfaces/survey';
|
||||
import { BaseEntity } from './base.entity';
|
||||
|
||||
@Entity({ name: 'surveyPublish' })
|
||||
export class ResponseSchema {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
export class ResponseSchema extends BaseEntity {
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
@ -43,21 +15,4 @@ export class ResponseSchema {
|
||||
|
||||
@Column()
|
||||
pageId: string;
|
||||
|
||||
@BeforeInsert()
|
||||
initDefaultInfo() {
|
||||
const now = Date.now();
|
||||
if (!this.curStatus) {
|
||||
const curStatus = { status: RECORD_STATUS.NEW, date: now };
|
||||
this.curStatus = curStatus;
|
||||
this.statusList = [curStatus];
|
||||
}
|
||||
this.createDate = now;
|
||||
this.updateDate = now;
|
||||
}
|
||||
|
||||
@BeforeUpdate()
|
||||
onUpdate() {
|
||||
this.updateDate = Date.now();
|
||||
}
|
||||
}
|
||||
|
@ -1,57 +1,11 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { SurveySchemaInterface } from '../interfaces/survey';
|
||||
|
||||
import { BaseEntity } from './base.entity';
|
||||
@Entity({ name: 'surveyConf' })
|
||||
export class SurveyConf {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
createDate: number;
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
updateDate: number;
|
||||
|
||||
export class SurveyConf extends BaseEntity {
|
||||
@Column('jsonb')
|
||||
code: SurveySchemaInterface;
|
||||
|
||||
@Column()
|
||||
pageId: string;
|
||||
|
||||
@BeforeInsert()
|
||||
initDefaultInfo() {
|
||||
const now = Date.now();
|
||||
if (!this.curStatus) {
|
||||
const curStatus = { status: RECORD_STATUS.NEW, date: now };
|
||||
this.curStatus = curStatus;
|
||||
this.statusList = [curStatus];
|
||||
}
|
||||
this.createDate = now;
|
||||
this.updateDate = now;
|
||||
}
|
||||
|
||||
@BeforeUpdate()
|
||||
onUpdate() {
|
||||
this.updateDate = Date.now();
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +1,10 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { HISTORY_TYPE, RECORD_STATUS } from '../enums';
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { HISTORY_TYPE } from '../enums';
|
||||
import { SurveySchemaInterface } from '../interfaces/survey';
|
||||
import { BaseEntity } from './base.entity';
|
||||
|
||||
@Entity({ name: 'surveyHistory' })
|
||||
export class SurveyHistory {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
export class SurveyHistory extends BaseEntity {
|
||||
@Column()
|
||||
pageId: string;
|
||||
|
||||
@ -46,21 +19,4 @@ export class SurveyHistory {
|
||||
username: string;
|
||||
_id: string;
|
||||
};
|
||||
|
||||
@BeforeInsert()
|
||||
initDefaultInfo() {
|
||||
const now = Date.now();
|
||||
if (!this.curStatus) {
|
||||
const curStatus = { status: RECORD_STATUS.NEW, date: now };
|
||||
this.curStatus = curStatus;
|
||||
this.statusList = [curStatus];
|
||||
}
|
||||
this.createDate = now;
|
||||
this.updateDate = now;
|
||||
}
|
||||
|
||||
@BeforeUpdate()
|
||||
onUpdate() {
|
||||
this.updateDate = Date.now();
|
||||
}
|
||||
}
|
||||
|
@ -1,36 +1,8 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { BaseEntity } from './base.entity';
|
||||
|
||||
@Entity({ name: 'surveyMeta' })
|
||||
export class SurveyMeta {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
export class SurveyMeta extends BaseEntity {
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
@ -54,21 +26,4 @@ export class SurveyMeta {
|
||||
|
||||
@Column()
|
||||
createFrom: string;
|
||||
|
||||
@BeforeInsert()
|
||||
initDefaultInfo() {
|
||||
const now = Date.now();
|
||||
if (!this.curStatus) {
|
||||
const curStatus = { status: RECORD_STATUS.NEW, date: now };
|
||||
this.curStatus = curStatus;
|
||||
this.statusList = [curStatus];
|
||||
}
|
||||
this.createDate = now;
|
||||
this.updateDate = now;
|
||||
}
|
||||
|
||||
@BeforeUpdate()
|
||||
onUpdate() {
|
||||
this.updateDate = Date.now();
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,9 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
AfterLoad,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
import { Entity, Column, BeforeInsert, AfterLoad } from 'typeorm';
|
||||
import pluginManager from '../securityPlugin/pluginManager';
|
||||
import { BaseEntity } from './base.entity';
|
||||
|
||||
@Entity({ name: 'surveySubmit' })
|
||||
export class SurveyResponse {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
export class SurveyResponse extends BaseEntity {
|
||||
@Column()
|
||||
pageId: string;
|
||||
|
||||
@ -36,44 +25,13 @@ export class SurveyResponse {
|
||||
@Column('jsonb')
|
||||
optionTextAndId: Record<string, any>;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
@BeforeInsert()
|
||||
initDefaultInfo() {
|
||||
const now = Date.now();
|
||||
if (!this.curStatus) {
|
||||
const curStatus = { status: RECORD_STATUS.NEW, date: now };
|
||||
this.curStatus = curStatus;
|
||||
this.statusList = [curStatus];
|
||||
}
|
||||
this.createDate = now;
|
||||
this.updateDate = now;
|
||||
pluginManager.triggerHook('beforeResponseDataCreate', this);
|
||||
}
|
||||
|
||||
@BeforeUpdate()
|
||||
onUpdate() {
|
||||
this.updateDate = Date.now();
|
||||
async onDataInsert() {
|
||||
return await pluginManager.triggerHook('beforeResponseDataCreate', this);
|
||||
}
|
||||
|
||||
@AfterLoad()
|
||||
onDataLoaded() {
|
||||
pluginManager.triggerHook('afterResponseDataReaded', this);
|
||||
async onDataLoaded() {
|
||||
return await pluginManager.triggerHook('afterResponseDataReaded', this);
|
||||
}
|
||||
}
|
||||
|
@ -1,56 +1,10 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { BaseEntity } from './base.entity';
|
||||
@Entity({ name: 'user' })
|
||||
export class User {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
export class User extends BaseEntity {
|
||||
@Column()
|
||||
username: string;
|
||||
|
||||
@Column()
|
||||
password: string;
|
||||
|
||||
@BeforeInsert()
|
||||
initDefaultInfo() {
|
||||
const now = Date.now();
|
||||
if (!this.curStatus) {
|
||||
const curStatus = { status: RECORD_STATUS.NEW, date: now };
|
||||
this.curStatus = curStatus;
|
||||
this.statusList = [curStatus];
|
||||
}
|
||||
this.createDate = now;
|
||||
this.updateDate = now;
|
||||
}
|
||||
|
||||
@BeforeUpdate()
|
||||
onUpdate() {
|
||||
this.updateDate = Date.now();
|
||||
}
|
||||
}
|
||||
|
@ -1,56 +1,10 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { BaseEntity } from './base.entity';
|
||||
@Entity({ name: 'word' })
|
||||
export class Word {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
export class Word extends BaseEntity {
|
||||
@Column()
|
||||
text: string;
|
||||
|
||||
@Column()
|
||||
type: string;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
@BeforeInsert()
|
||||
initDefaultInfo() {
|
||||
const now = Date.now();
|
||||
if (!this.curStatus) {
|
||||
const curStatus = { status: RECORD_STATUS.NEW, date: now };
|
||||
this.curStatus = curStatus;
|
||||
this.statusList = [curStatus];
|
||||
}
|
||||
this.createDate = now;
|
||||
this.updateDate = now;
|
||||
}
|
||||
|
||||
@BeforeUpdate()
|
||||
onUpdate() {
|
||||
this.updateDate = Date.now();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthController } from '../controllers/auth.controller';
|
||||
import { UserService } from '../services/user.service';
|
||||
import { CaptchaService } from '../services/captcha.service';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
@ -10,6 +10,7 @@ import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { User } from 'src/models/user.entity';
|
||||
import { Captcha } from 'src/models/captcha.entity';
|
||||
|
||||
jest.mock('../services/captcha.service');
|
||||
jest.mock('../services/auth.service');
|
||||
@ -23,7 +24,7 @@ describe('AuthController', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [ConfigModule.forRoot()],
|
||||
// imports: [ConfigModule.forRoot()],
|
||||
controllers: [AuthController],
|
||||
providers: [UserService, CaptchaService, ConfigService, AuthService],
|
||||
}).compile();
|
||||
@ -149,4 +150,19 @@ describe('AuthController', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCaptcha', () => {
|
||||
it('should return captcha image and id', async () => {
|
||||
const captcha = new Captcha();
|
||||
const mockCaptchaId = new ObjectId();
|
||||
captcha._id = mockCaptchaId;
|
||||
jest.spyOn(captchaService, 'createCaptcha').mockResolvedValue(captcha);
|
||||
|
||||
const result = await controller.getCaptcha();
|
||||
|
||||
expect(result.code).toBe(200);
|
||||
expect(result.data.id).toBe(mockCaptchaId.toString());
|
||||
expect(typeof result.data.img).toBe('string');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { sign } from 'jsonwebtoken';
|
||||
|
||||
jest.mock('jsonwebtoken');
|
@ -1,5 +1,5 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CaptchaService } from './captcha.service';
|
||||
import { CaptchaService } from '../services/captcha.service';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { Captcha } from 'src/models/captcha.entity';
|
||||
import { ObjectId } from 'mongodb';
|
142
server/src/modules/auth/__test/user.service.spec.ts
Normal file
142
server/src/modules/auth/__test/user.service.spec.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { UserService } from '../services/user.service';
|
||||
import { User } from 'src/models/user.entity';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { hash256 } from 'src/utils/hash256';
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
let userRepository: MongoRepository<User>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
UserService,
|
||||
{
|
||||
provide: getRepositoryToken(User),
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UserService>(UserService);
|
||||
userRepository = module.get<MongoRepository<User>>(
|
||||
getRepositoryToken(User),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a user', async () => {
|
||||
const userInfo = {
|
||||
username: 'testUser',
|
||||
password: 'testPassword',
|
||||
} as User;
|
||||
|
||||
const createSpy = jest
|
||||
.spyOn(userRepository, 'create')
|
||||
.mockImplementation(() => userInfo);
|
||||
const saveSpy = jest
|
||||
.spyOn(userRepository, 'save')
|
||||
.mockResolvedValue(userInfo);
|
||||
const findOneSpy = jest
|
||||
.spyOn(userRepository, 'findOne')
|
||||
.mockResolvedValue(null);
|
||||
|
||||
const user = await service.createUser(userInfo);
|
||||
|
||||
expect(findOneSpy).toHaveBeenCalledWith({
|
||||
where: { username: userInfo.username },
|
||||
});
|
||||
expect(createSpy).toHaveBeenCalledWith({
|
||||
username: userInfo.username,
|
||||
password: expect.any(String),
|
||||
});
|
||||
expect(saveSpy).toHaveBeenCalled();
|
||||
expect(user).toEqual(userInfo);
|
||||
});
|
||||
|
||||
it('should throw when trying to create an existing user', async () => {
|
||||
const userInfo = {
|
||||
username: 'existingUser',
|
||||
password: 'existingPassword',
|
||||
} as User;
|
||||
|
||||
const findOneSpy = jest
|
||||
.spyOn(userRepository, 'findOne')
|
||||
.mockResolvedValue(userInfo);
|
||||
|
||||
await expect(service.createUser(userInfo)).rejects.toThrow(HttpException);
|
||||
expect(findOneSpy).toHaveBeenCalledWith({
|
||||
where: { username: userInfo.username },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a user by credentials', async () => {
|
||||
const userInfo = {
|
||||
username: 'existingUser',
|
||||
password: 'existingPassword',
|
||||
};
|
||||
|
||||
const hashedPassword = hash256(userInfo.password);
|
||||
jest.spyOn(userRepository, 'findOne').mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
username: userInfo.username,
|
||||
password: hashedPassword,
|
||||
} as User);
|
||||
});
|
||||
|
||||
const user = await service.getUser(userInfo);
|
||||
|
||||
expect(userRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
username: userInfo.username,
|
||||
password: hashedPassword,
|
||||
},
|
||||
});
|
||||
expect(user).toEqual({ ...userInfo, password: hashedPassword });
|
||||
});
|
||||
|
||||
it('should return undefined when user is not found by credentials', async () => {
|
||||
const userInfo = {
|
||||
username: 'nonExistingUser',
|
||||
password: 'nonExistingPassword',
|
||||
};
|
||||
|
||||
const hashedPassword = hash256(userInfo.password);
|
||||
const findOneSpy = jest
|
||||
.spyOn(userRepository, 'findOne')
|
||||
.mockResolvedValue(null);
|
||||
|
||||
const user = await service.getUser(userInfo);
|
||||
|
||||
expect(findOneSpy).toHaveBeenCalledWith({
|
||||
where: {
|
||||
username: userInfo.username,
|
||||
password: hashedPassword,
|
||||
},
|
||||
});
|
||||
expect(user).toBe(null);
|
||||
});
|
||||
|
||||
it('should return a user by username', async () => {
|
||||
const username = 'existingUser';
|
||||
const userInfo = {
|
||||
username: username,
|
||||
password: 'existingPassword',
|
||||
} as User;
|
||||
|
||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(userInfo);
|
||||
|
||||
const user = await service.getUserByUsername(username);
|
||||
|
||||
expect(userRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { username: username },
|
||||
});
|
||||
expect(user).toEqual(userInfo);
|
||||
});
|
||||
});
|
@ -2,9 +2,9 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { User } from 'src/models/user.entity';
|
||||
import { createHash } from 'crypto';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { hash256 } from 'src/utils/hash256';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
@ -13,10 +13,6 @@ export class UserService {
|
||||
private readonly userRepository: MongoRepository<User>,
|
||||
) {}
|
||||
|
||||
private hash256(text) {
|
||||
return createHash('sha256').update(text).digest('hex');
|
||||
}
|
||||
|
||||
async createUser(userInfo: {
|
||||
username: string;
|
||||
password: string;
|
||||
@ -31,7 +27,7 @@ export class UserService {
|
||||
|
||||
const newUser = this.userRepository.create({
|
||||
username: userInfo.username,
|
||||
password: this.hash256(userInfo.password),
|
||||
password: hash256(userInfo.password),
|
||||
});
|
||||
|
||||
return this.userRepository.save(newUser);
|
||||
@ -44,7 +40,7 @@ export class UserService {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
username: userInfo.username,
|
||||
password: this.hash256(userInfo.password), // Please handle password hashing here
|
||||
password: hash256(userInfo.password), // Please handle password hashing here
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -0,0 +1,158 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { DataStatisticController } from '../controllers/dataStatistic.controller';
|
||||
import { DataStatisticService } from '../services/dataStatistic.service';
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
|
||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { Authtication } from 'src/guards/authtication';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
||||
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
jest.mock('../services/dataStatistic.service');
|
||||
jest.mock('../services/surveyMeta.service');
|
||||
jest.mock('../../surveyResponse/services/responseScheme.service');
|
||||
|
||||
describe('DataStatisticController', () => {
|
||||
let controller: DataStatisticController;
|
||||
let dataStatisticService: DataStatisticService;
|
||||
let surveyMetaService: SurveyMetaService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [DataStatisticController],
|
||||
providers: [
|
||||
DataStatisticService,
|
||||
SurveyMetaService,
|
||||
ResponseSchemaService,
|
||||
PluginManagerProvider,
|
||||
ConfigService,
|
||||
{
|
||||
provide: Authtication,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
canActivate: () => true,
|
||||
})),
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
getUserByUsername() {
|
||||
return {};
|
||||
},
|
||||
})),
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<DataStatisticController>(DataStatisticController);
|
||||
dataStatisticService =
|
||||
module.get<DataStatisticService>(DataStatisticService);
|
||||
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
|
||||
const pluginManager = module.get<XiaojuSurveyPluginManager>(
|
||||
XiaojuSurveyPluginManager,
|
||||
);
|
||||
pluginManager.registerPlugin(
|
||||
new ResponseSecurityPlugin('dataAesEncryptSecretKey'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
describe('data', () => {
|
||||
it('should return data table', async () => {
|
||||
const surveyId = new ObjectId().toString();
|
||||
const mockRequest = {
|
||||
query: {
|
||||
surveyId,
|
||||
},
|
||||
user: {
|
||||
username: 'testUser',
|
||||
},
|
||||
};
|
||||
|
||||
const mockDataTable = {
|
||||
total: 10,
|
||||
listHead: [
|
||||
{
|
||||
field: 'xxx',
|
||||
title: 'xxx',
|
||||
type: 'xxx',
|
||||
othersCode: 'xxx',
|
||||
},
|
||||
],
|
||||
listBody: [
|
||||
{ difTime: '0.5', createDate: '2024-02-11' },
|
||||
{ difTime: '0.5', createDate: '2024-02-11' },
|
||||
],
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockResolvedValueOnce(undefined);
|
||||
jest
|
||||
.spyOn(controller['responseSchemaService'], 'getResponseSchemaByPageId')
|
||||
.mockResolvedValueOnce({} as any);
|
||||
jest
|
||||
.spyOn(dataStatisticService, 'getDataTable')
|
||||
.mockResolvedValueOnce(mockDataTable);
|
||||
|
||||
const result = await controller.data(mockRequest.query, mockRequest);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
data: mockDataTable,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return data table with isDesensitive', async () => {
|
||||
const surveyId = new ObjectId().toString();
|
||||
const mockRequest = {
|
||||
query: {
|
||||
surveyId,
|
||||
isDesensitive: true,
|
||||
},
|
||||
user: {
|
||||
username: 'testUser',
|
||||
},
|
||||
};
|
||||
|
||||
const mockDataTable = {
|
||||
total: 10,
|
||||
listHead: [
|
||||
{
|
||||
field: 'xxx',
|
||||
title: 'xxx',
|
||||
type: 'xxx',
|
||||
othersCode: 'xxx',
|
||||
},
|
||||
],
|
||||
listBody: [
|
||||
{ difTime: '0.5', createDate: '2024-02-11', data123: '15200000000' },
|
||||
{ difTime: '0.5', createDate: '2024-02-11', data123: '13800000000' },
|
||||
],
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockResolvedValueOnce(undefined);
|
||||
jest
|
||||
.spyOn(controller['responseSchemaService'], 'getResponseSchemaByPageId')
|
||||
.mockResolvedValueOnce({} as any);
|
||||
jest
|
||||
.spyOn(dataStatisticService, 'getDataTable')
|
||||
.mockResolvedValueOnce(mockDataTable);
|
||||
|
||||
const result = await controller.data(mockRequest.query, mockRequest);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
data: mockDataTable,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
313
server/src/modules/survey/__test/dataStatistic.service.spec.ts
Normal file
313
server/src/modules/survey/__test/dataStatistic.service.spec.ts
Normal file
@ -0,0 +1,313 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { DataStatisticService } from '../services/dataStatistic.service';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||
import {
|
||||
mockResponseSchema,
|
||||
mockSensitiveResponseSchema,
|
||||
} from './mockResponseSchema';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
||||
|
||||
describe('DataStatisticService', () => {
|
||||
let service: DataStatisticService;
|
||||
let surveyResponseRepository: MongoRepository<SurveyResponse>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
DataStatisticService,
|
||||
{
|
||||
provide: getRepositoryToken(SurveyResponse),
|
||||
useClass: MongoRepository,
|
||||
},
|
||||
PluginManagerProvider,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<DataStatisticService>(DataStatisticService);
|
||||
surveyResponseRepository = module.get<MongoRepository<SurveyResponse>>(
|
||||
getRepositoryToken(SurveyResponse),
|
||||
);
|
||||
const manager = module.get<XiaojuSurveyPluginManager>(
|
||||
XiaojuSurveyPluginManager,
|
||||
);
|
||||
manager.registerPlugin(
|
||||
new ResponseSecurityPlugin('dataAesEncryptSecretKey'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getDataTable', () => {
|
||||
it('should return correct table data', async () => {
|
||||
const surveyId = '65afc62904d5db18534c0f78';
|
||||
const pageNum = 1;
|
||||
const pageSize = 10;
|
||||
|
||||
const responseSchema = mockResponseSchema;
|
||||
const surveyResponseList = [
|
||||
{
|
||||
_id: new ObjectId('65f1baff92862d6a9067ad0c'),
|
||||
pageId: '65afc62904d5db18534c0f78',
|
||||
surveyPath: 'JgMLGInV',
|
||||
data: {
|
||||
data458: '111',
|
||||
data549: '222',
|
||||
data515_115019: '333',
|
||||
data515: '115019',
|
||||
data997_211974: '444',
|
||||
data997: ['211974', '842501'],
|
||||
data517: '917392',
|
||||
data413_3: '555',
|
||||
data413: 3,
|
||||
data863: '109239',
|
||||
},
|
||||
difTime: 21278,
|
||||
clientTime: 1710340862733.0,
|
||||
secretKeys: [],
|
||||
optionTextAndId: {
|
||||
data549: [
|
||||
{
|
||||
hash: '273008',
|
||||
text: '选项1',
|
||||
},
|
||||
{
|
||||
hash: '160703',
|
||||
text: '选项2',
|
||||
},
|
||||
],
|
||||
data515: [
|
||||
{
|
||||
hash: '115019',
|
||||
text: '<p>选项1</p>',
|
||||
},
|
||||
{
|
||||
hash: '115020',
|
||||
text: '<p>选项2</p>',
|
||||
},
|
||||
{
|
||||
hash: '119074',
|
||||
text: '<p>选项</p>',
|
||||
},
|
||||
],
|
||||
data997: [
|
||||
{
|
||||
hash: '211974',
|
||||
text: '<p>选项1</p>',
|
||||
},
|
||||
{
|
||||
hash: '842501',
|
||||
text: '<p>选项2</p>',
|
||||
},
|
||||
{
|
||||
hash: '650873',
|
||||
text: '<p>选项</p>',
|
||||
},
|
||||
],
|
||||
data517: [
|
||||
{
|
||||
hash: '917392',
|
||||
text: '对',
|
||||
},
|
||||
{
|
||||
hash: '156728',
|
||||
text: '错',
|
||||
},
|
||||
],
|
||||
data413: [
|
||||
{
|
||||
hash: '502734',
|
||||
text: '选项1',
|
||||
},
|
||||
{
|
||||
hash: '278946',
|
||||
text: '选项2',
|
||||
},
|
||||
],
|
||||
data863: [
|
||||
{
|
||||
hash: '109239',
|
||||
text: '<p>选项1</p>',
|
||||
},
|
||||
{
|
||||
hash: '899262',
|
||||
text: '<p>选项2</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
curStatus: {
|
||||
status: RECORD_STATUS.NEW,
|
||||
date: 1710340863123.0,
|
||||
},
|
||||
statusList: [
|
||||
{
|
||||
status: RECORD_STATUS.NEW,
|
||||
date: 1710340863123.0,
|
||||
},
|
||||
],
|
||||
createDate: 1710340863123.0,
|
||||
updateDate: 1710340863123.0,
|
||||
},
|
||||
] as unknown as Array<SurveyResponse>;
|
||||
|
||||
jest
|
||||
.spyOn(surveyResponseRepository, 'findAndCount')
|
||||
.mockResolvedValue([surveyResponseList, surveyResponseList.length]);
|
||||
|
||||
const result = await service.getDataTable({
|
||||
surveyId,
|
||||
pageNum,
|
||||
pageSize,
|
||||
responseSchema,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
total: 1,
|
||||
listHead: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: expect.any(String),
|
||||
title: expect.any(String),
|
||||
type: expect.stringMatching(
|
||||
/^(text|textarea|radio|checkbox|binary-choice|radio-star|vote)$/,
|
||||
),
|
||||
othersCode: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: expect.any(String),
|
||||
option: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
listBody: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
data458: expect.any(String),
|
||||
data549: expect.any(String),
|
||||
data515_115019: expect.any(String),
|
||||
data515: expect.any(String),
|
||||
data997_211974: expect.any(String),
|
||||
data997: expect.any(String),
|
||||
data517: expect.any(String),
|
||||
data413_3: expect.any(String),
|
||||
data413: expect.any(Number),
|
||||
data863: expect.any(String),
|
||||
data413_custom: expect.any(String),
|
||||
difTime: expect.any(String),
|
||||
createDate: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return desensitive table data', async () => {
|
||||
const mockSchema = cloneDeep(mockSensitiveResponseSchema);
|
||||
const surveyResponseList: Array<SurveyResponse> = [
|
||||
{
|
||||
_id: new ObjectId('65f2a2e892862d6a9067ad29'),
|
||||
pageId: '65f29f3192862d6a9067ad1c',
|
||||
surveyPath: 'EBzdmnSp',
|
||||
data: {
|
||||
data458: 'U2FsdGVkX18IlyS9gSKNTAG0llOVQmrGUzRn/r95VKw=',
|
||||
data515: '115019',
|
||||
data450:
|
||||
'U2FsdGVkX1+ArNkHhqSmHrCWWT2oxTGBlyTcXdJfQTwqBouROeITBx/aAp7pjKk4',
|
||||
data405:
|
||||
'U2FsdGVkX19bRmf3uEmXAJ/6zXd1Znr3cZsD5v4Nocr2v5XG1taXluz8cohFkDyH',
|
||||
data770: 'U2FsdGVkX18ldQMhJjFXO8aerjftZLpFnRQ4/FVcCLI=',
|
||||
},
|
||||
difTime: 806707,
|
||||
clientTime: 1710400229573.0,
|
||||
secretKeys: ['data458', 'data450', 'data405', 'data770'],
|
||||
optionTextAndId: {
|
||||
data515: [
|
||||
{
|
||||
hash: '115019',
|
||||
text: '<p>男</p>',
|
||||
},
|
||||
{
|
||||
hash: '115020',
|
||||
text: '<p>女</p>',
|
||||
},
|
||||
],
|
||||
data450: [
|
||||
{
|
||||
hash: '979954',
|
||||
text: '选项1',
|
||||
},
|
||||
{
|
||||
hash: '083007',
|
||||
text: '选项2',
|
||||
},
|
||||
],
|
||||
data405: [
|
||||
{
|
||||
hash: '443109',
|
||||
text: '选项1',
|
||||
},
|
||||
{
|
||||
hash: '871142',
|
||||
text: '选项2',
|
||||
},
|
||||
],
|
||||
data770: [
|
||||
{
|
||||
hash: '051056',
|
||||
text: '选项1',
|
||||
},
|
||||
{
|
||||
hash: '835356',
|
||||
text: '选项2',
|
||||
},
|
||||
],
|
||||
},
|
||||
curStatus: {
|
||||
status: RECORD_STATUS.NEW,
|
||||
date: 1710400232161.0,
|
||||
},
|
||||
statusList: [
|
||||
{
|
||||
status: RECORD_STATUS.NEW,
|
||||
date: 1710400232161.0,
|
||||
},
|
||||
],
|
||||
createDate: 1710400232161.0,
|
||||
updateDate: 1710400232161.0,
|
||||
},
|
||||
] as unknown as Array<SurveyResponse>;
|
||||
|
||||
const surveyId = mockSchema.pageId;
|
||||
const pageNum = 1;
|
||||
const pageSize = 10;
|
||||
|
||||
jest
|
||||
.spyOn(surveyResponseRepository, 'findAndCount')
|
||||
.mockResolvedValue([surveyResponseList, surveyResponseList.length]);
|
||||
|
||||
const result = await service.getDataTable({
|
||||
surveyId,
|
||||
pageNum,
|
||||
pageSize,
|
||||
responseSchema: mockSchema,
|
||||
});
|
||||
expect(result.listBody).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
createDate: expect.any(String),
|
||||
data405: expect.any(String),
|
||||
data450: expect.any(String),
|
||||
data458: expect.any(String),
|
||||
data515: expect.any(String),
|
||||
data770: expect.any(String),
|
||||
difTime: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
637
server/src/modules/survey/__test/mockResponseSchema.ts
Normal file
637
server/src/modules/survey/__test/mockResponseSchema.ts
Normal file
@ -0,0 +1,637 @@
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
export const mockSensitiveResponseSchema: ResponseSchema = {
|
||||
_id: new ObjectId('65f29f8892862d6a9067ad25'),
|
||||
curStatus: {
|
||||
status: RECORD_STATUS.PUBLISHED,
|
||||
date: 1710399368439,
|
||||
},
|
||||
statusList: [
|
||||
{
|
||||
status: RECORD_STATUS.PUBLISHED,
|
||||
date: 1710399368439,
|
||||
},
|
||||
],
|
||||
createDate: 1710399368440,
|
||||
updateDate: 1710399368440,
|
||||
title: '加密全流程',
|
||||
surveyPath: 'EBzdmnSp',
|
||||
code: {
|
||||
bannerConf: {
|
||||
titleConfig: {
|
||||
mainTitle:
|
||||
'<h3 style="text-align: center">欢迎填写问卷</h3><p>为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,<span style="color: rgb(204, 0, 0)">期待您的参与!</span></p>',
|
||||
subTitle: '',
|
||||
},
|
||||
bannerConfig: {
|
||||
bgImage: '/imgs/skin/17e06b7604a007e1d3e1453b9ddadc3c.webp',
|
||||
videoLink: '',
|
||||
postImg: '',
|
||||
},
|
||||
},
|
||||
baseConf: {
|
||||
begTime: '2024-03-14 14:54:41',
|
||||
endTime: '2034-03-14 14:54:41',
|
||||
language: 'chinese',
|
||||
tLimit: 0,
|
||||
answerBegTime: '',
|
||||
answerEndTime: '',
|
||||
},
|
||||
bottomConf: {
|
||||
logoImage: '/imgs/Logo.webp',
|
||||
logoImageWidth: '60%',
|
||||
},
|
||||
skinConf: {
|
||||
skinColor: '#4a4c5b',
|
||||
inputBgColor: '#ffffff',
|
||||
},
|
||||
submitConf: {
|
||||
submitTitle: '提交',
|
||||
msgContent: {
|
||||
msg_200: '提交成功',
|
||||
msg_9001: '您来晚了,感谢支持问卷~',
|
||||
msg_9002: '请勿多次提交!',
|
||||
msg_9003: '您来晚了,已经满额!',
|
||||
msg_9004: '提交失败!',
|
||||
},
|
||||
confirmAgain: {
|
||||
is_again: true,
|
||||
again_text: '确认要提交吗?',
|
||||
},
|
||||
},
|
||||
dataConf: {
|
||||
dataList: [
|
||||
{
|
||||
isRequired: true,
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'text',
|
||||
valid: '',
|
||||
field: 'data458',
|
||||
title: '<p>您的手机号</p>',
|
||||
placeholder: '',
|
||||
randomSort: false,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
star: 5,
|
||||
nps: {
|
||||
leftText: '极不满意',
|
||||
rightText: '极满意',
|
||||
},
|
||||
placeholderDesc: '',
|
||||
textRange: {
|
||||
min: {
|
||||
placeholder: '0',
|
||||
value: 0,
|
||||
},
|
||||
max: {
|
||||
placeholder: '500',
|
||||
value: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
isRequired: true,
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'radio',
|
||||
placeholderDesc: '',
|
||||
field: 'data515',
|
||||
title: '<p>您的性别</p>',
|
||||
placeholder: '',
|
||||
randomSort: false,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
options: [
|
||||
{
|
||||
text: '<p>男</p>',
|
||||
others: false,
|
||||
mustOthers: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '115019',
|
||||
},
|
||||
{
|
||||
text: '<p>女</p>',
|
||||
others: false,
|
||||
mustOthers: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '115020',
|
||||
},
|
||||
],
|
||||
importKey: 'single',
|
||||
importData: '',
|
||||
cOption: '',
|
||||
cOptions: [],
|
||||
nps: {
|
||||
leftText: '极不满意',
|
||||
rightText: '极满意',
|
||||
},
|
||||
star: 5,
|
||||
exclude: false,
|
||||
textRange: {
|
||||
min: {
|
||||
placeholder: '0',
|
||||
value: 0,
|
||||
},
|
||||
max: {
|
||||
placeholder: '500',
|
||||
value: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'data450',
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'text',
|
||||
placeholderDesc: '',
|
||||
title: '<p>身份证</p>',
|
||||
placeholder: '',
|
||||
valid: '',
|
||||
isRequired: true,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
starStyle: 'star',
|
||||
rangeConfig: {},
|
||||
options: [
|
||||
{
|
||||
text: '选项1',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '979954',
|
||||
},
|
||||
{
|
||||
text: '选项2',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '083007',
|
||||
},
|
||||
],
|
||||
star: 5,
|
||||
textRange: {
|
||||
min: {
|
||||
placeholder: '0',
|
||||
value: 0,
|
||||
},
|
||||
max: {
|
||||
placeholder: '500',
|
||||
value: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'data405',
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'text',
|
||||
placeholderDesc: '',
|
||||
title: '<p>地址</p>',
|
||||
placeholder: '',
|
||||
valid: '',
|
||||
isRequired: true,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
starStyle: 'star',
|
||||
rangeConfig: {},
|
||||
options: [
|
||||
{
|
||||
text: '选项1',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '443109',
|
||||
},
|
||||
{
|
||||
text: '选项2',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '871142',
|
||||
},
|
||||
],
|
||||
star: 5,
|
||||
textRange: {
|
||||
min: {
|
||||
placeholder: '0',
|
||||
value: 0,
|
||||
},
|
||||
max: {
|
||||
placeholder: '500',
|
||||
value: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'data770',
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'text',
|
||||
placeholderDesc: '',
|
||||
title: '<p>邮箱</p>',
|
||||
placeholder: '',
|
||||
valid: '',
|
||||
isRequired: true,
|
||||
randomSort: false,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
starStyle: 'star',
|
||||
rangeConfig: {},
|
||||
options: [
|
||||
{
|
||||
text: '选项1',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '051056',
|
||||
},
|
||||
{
|
||||
text: '选项2',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '835356',
|
||||
},
|
||||
],
|
||||
star: 5,
|
||||
textRange: {
|
||||
min: {
|
||||
placeholder: '0',
|
||||
value: 0,
|
||||
},
|
||||
max: {
|
||||
placeholder: '500',
|
||||
value: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
pageId: '65f29f3192862d6a9067ad1c',
|
||||
} as ResponseSchema;
|
||||
|
||||
export const mockResponseSchema: ResponseSchema = {
|
||||
_id: new ObjectId('65b0d46e04d5db18534c0f7c'),
|
||||
curStatus: {
|
||||
status: RECORD_STATUS.PUBLISHED,
|
||||
date: 1710340841287.0,
|
||||
},
|
||||
statusList: [
|
||||
{
|
||||
status: RECORD_STATUS.PUBLISHED,
|
||||
date: 1706018345927.0,
|
||||
},
|
||||
],
|
||||
title: '新系统创建的',
|
||||
surveyPath: 'JgMLGInV',
|
||||
code: {
|
||||
bannerConf: {
|
||||
titleConfig: {
|
||||
mainTitle:
|
||||
'<h3 style="text-align: center">欢迎填写问卷</h3><p>为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,<span style="color: rgb(204, 0, 0)">期待您的参与!</span></p>',
|
||||
subTitle: '',
|
||||
},
|
||||
bannerConfig: {
|
||||
bgImage:
|
||||
'http://10.190.55.101:3000/imgs/skin/17e06b7604a007e1d3e1453b9ddadc3c.webp',
|
||||
videoLink: '',
|
||||
postImg: '',
|
||||
},
|
||||
},
|
||||
baseConf: {
|
||||
begTime: '2024-01-23 21:59:05',
|
||||
endTime: '2034-01-23 21:59:05',
|
||||
language: 'chinese',
|
||||
tLimit: 0,
|
||||
answerBegTime: '',
|
||||
answerEndTime: '',
|
||||
},
|
||||
bottomConf: {
|
||||
logoImage: '/imgs/Logo.webp',
|
||||
logoImageWidth: '60%',
|
||||
},
|
||||
skinConf: {
|
||||
skinColor: '#4a4c5b',
|
||||
inputBgColor: '#ffffff',
|
||||
},
|
||||
submitConf: {
|
||||
submitTitle: '提交',
|
||||
msgContent: {
|
||||
msg_200: '<p>提交成功</p>',
|
||||
msg_9001: '您来晚了,感谢支持问卷~',
|
||||
msg_9002: '请勿多次提交!',
|
||||
msg_9003: '您来晚了,已经满额!',
|
||||
msg_9004: '提交失败!',
|
||||
},
|
||||
confirmAgain: {
|
||||
is_again: true,
|
||||
again_text: '确认要提交吗?',
|
||||
},
|
||||
},
|
||||
dataConf: {
|
||||
dataList: [
|
||||
{
|
||||
isRequired: true,
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'text',
|
||||
valid: '',
|
||||
field: 'data458',
|
||||
title: '标题1',
|
||||
placeholder: '',
|
||||
randomSort: false,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
star: 5,
|
||||
nps: {
|
||||
leftText: '极不满意',
|
||||
rightText: '极满意',
|
||||
},
|
||||
placeholderDesc: '',
|
||||
},
|
||||
{
|
||||
field: 'data549',
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'textarea',
|
||||
placeholderDesc: '',
|
||||
title: '标题2',
|
||||
placeholder: '',
|
||||
valid: '',
|
||||
isRequired: true,
|
||||
randomSort: false,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
starStyle: 'star',
|
||||
rangeConfig: {},
|
||||
options: [
|
||||
{
|
||||
text: '选项1',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '273008',
|
||||
},
|
||||
{
|
||||
text: '选项2',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '160703',
|
||||
},
|
||||
],
|
||||
star: 5,
|
||||
},
|
||||
{
|
||||
isRequired: true,
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'radio',
|
||||
placeholderDesc: '',
|
||||
field: 'data515',
|
||||
title: '标题2',
|
||||
placeholder: '',
|
||||
randomSort: false,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
options: [
|
||||
{
|
||||
text: '<p>选项1</p>',
|
||||
others: true,
|
||||
mustOthers: false,
|
||||
othersKey: 'data515_115019',
|
||||
placeholderDesc: '',
|
||||
hash: '115019',
|
||||
},
|
||||
{
|
||||
text: '<p>选项2</p>',
|
||||
others: false,
|
||||
mustOthers: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '115020',
|
||||
},
|
||||
{
|
||||
text: '<p>选项</p>',
|
||||
others: false,
|
||||
mustOthers: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '119074',
|
||||
},
|
||||
],
|
||||
importKey: 'single',
|
||||
importData: '',
|
||||
cOption: '',
|
||||
cOptions: [],
|
||||
nps: {
|
||||
leftText: '极不满意',
|
||||
rightText: '极满意',
|
||||
},
|
||||
star: 5,
|
||||
},
|
||||
{
|
||||
field: 'data997',
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'checkbox',
|
||||
placeholderDesc: '',
|
||||
title: '标题4',
|
||||
placeholder: '',
|
||||
valid: '',
|
||||
isRequired: true,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
starStyle: 'star',
|
||||
rangeConfig: {},
|
||||
options: [
|
||||
{
|
||||
text: '<p>选项1</p>',
|
||||
others: true,
|
||||
othersKey: 'data997_211974',
|
||||
placeholderDesc: '',
|
||||
hash: '211974',
|
||||
},
|
||||
{
|
||||
text: '<p>选项2</p>',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '842501',
|
||||
},
|
||||
{
|
||||
text: '<p>选项</p>',
|
||||
others: false,
|
||||
othersKey: 'data997_211974',
|
||||
placeholderDesc: '',
|
||||
hash: '650873',
|
||||
},
|
||||
],
|
||||
star: 5,
|
||||
},
|
||||
{
|
||||
field: 'data517',
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'binary-choice',
|
||||
placeholderDesc: '',
|
||||
title: '标题5',
|
||||
placeholder: '',
|
||||
valid: '',
|
||||
isRequired: true,
|
||||
randomSort: false,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
starStyle: 'star',
|
||||
rangeConfig: {},
|
||||
options: [
|
||||
{
|
||||
text: '对',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '917392',
|
||||
},
|
||||
{
|
||||
text: '错',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '156728',
|
||||
},
|
||||
],
|
||||
star: 5,
|
||||
},
|
||||
{
|
||||
field: 'data413',
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'radio-star',
|
||||
placeholderDesc: '',
|
||||
title: '标题6',
|
||||
placeholder: '',
|
||||
valid: '',
|
||||
isRequired: true,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
starStyle: 'star',
|
||||
rangeConfig: {
|
||||
'1': {
|
||||
isShowInput: true,
|
||||
text: '',
|
||||
required: false,
|
||||
explain: '',
|
||||
},
|
||||
'2': {
|
||||
isShowInput: true,
|
||||
text: '',
|
||||
required: false,
|
||||
explain: '',
|
||||
},
|
||||
'3': {
|
||||
isShowInput: true,
|
||||
text: '',
|
||||
required: false,
|
||||
explain: '',
|
||||
},
|
||||
'4': {
|
||||
isShowInput: false,
|
||||
text: '',
|
||||
required: false,
|
||||
explain: '',
|
||||
},
|
||||
'5': {
|
||||
isShowInput: false,
|
||||
text: '',
|
||||
required: false,
|
||||
explain: '',
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
text: '选项1',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '502734',
|
||||
},
|
||||
{
|
||||
text: '选项2',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '278946',
|
||||
},
|
||||
],
|
||||
star: 5,
|
||||
},
|
||||
{
|
||||
field: 'data863',
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'vote',
|
||||
placeholderDesc: '',
|
||||
title: '标题7',
|
||||
placeholder: '',
|
||||
valid: '',
|
||||
isRequired: true,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
starStyle: 'star',
|
||||
rangeConfig: {},
|
||||
options: [
|
||||
{
|
||||
text: '<p>选项1</p>',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '109239',
|
||||
},
|
||||
{
|
||||
text: '<p>选项2</p>',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '899262',
|
||||
},
|
||||
],
|
||||
star: 5,
|
||||
innerType: 'radio',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
pageId: '65afc62904d5db18534c0f78',
|
||||
createDate: 1710340841289,
|
||||
updateDate: 1710340841289.0,
|
||||
} as ResponseSchema;
|
@ -8,6 +8,8 @@ import { SurveyHistoryService } from '../services/surveyHistory.service';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||
import { SurveyConf } from 'src/models/surveyConf.entity';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
|
||||
// Mock the services
|
||||
jest.mock('../services/surveyMeta.service');
|
||||
@ -186,8 +188,6 @@ describe('SurveyController', () => {
|
||||
code: 200,
|
||||
});
|
||||
});
|
||||
|
||||
// Add more test cases for different scenarios
|
||||
});
|
||||
|
||||
describe('deleteSurvey', () => {
|
||||
@ -218,8 +218,6 @@ describe('SurveyController', () => {
|
||||
code: 200,
|
||||
});
|
||||
});
|
||||
|
||||
// Add more test cases for different scenarios
|
||||
});
|
||||
|
||||
describe('getSurvey', () => {
|
||||
@ -255,7 +253,7 @@ describe('SurveyController', () => {
|
||||
});
|
||||
|
||||
describe('publishSurvey', () => {
|
||||
it('should publish a survey and its response schema', async () => {
|
||||
it('should publish a survey success', async () => {
|
||||
const surveyId = new ObjectId();
|
||||
const surveyMeta = {
|
||||
_id: surveyId,
|
||||
@ -304,5 +302,49 @@ describe('SurveyController', () => {
|
||||
code: 200,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not publish a survey with forbidden content', async () => {
|
||||
const surveyId = new ObjectId();
|
||||
const surveyMeta = {
|
||||
_id: surveyId,
|
||||
surveyType: 'normal',
|
||||
owner: 'testUser',
|
||||
} as SurveyMeta;
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockResolvedValue(Promise.resolve(surveyMeta));
|
||||
|
||||
jest
|
||||
.spyOn(surveyConfService, 'getSurveyConfBySurveyId')
|
||||
.mockResolvedValue(
|
||||
Promise.resolve({
|
||||
_id: new ObjectId(),
|
||||
pageId: surveyId.toString(),
|
||||
} as SurveyConf),
|
||||
);
|
||||
|
||||
jest
|
||||
.spyOn(surveyConfService, 'getSurveyContentByCode')
|
||||
.mockResolvedValue({
|
||||
text: '违禁词',
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(contentSecurityService, 'isForbiddenContent')
|
||||
.mockResolvedValue(true);
|
||||
|
||||
await expect(
|
||||
controller.publishSurvey(
|
||||
{ surveyId: surveyId.toString() },
|
||||
{ user: { username: 'testUser', _id: 'testUserId' } },
|
||||
),
|
||||
).rejects.toThrow(
|
||||
new HttpException(
|
||||
'问卷存在非法关键字,不允许发布',
|
||||
EXCEPTION_CODE.SURVEY_CONTENT_NOT_ALLOW,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
152
server/src/modules/survey/__test/surveyConf.service.spec.ts
Normal file
152
server/src/modules/survey/__test/surveyConf.service.spec.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { SurveyConf } from 'src/models/surveyConf.entity';
|
||||
import { SurveyConfService } from '../services/surveyConf.service';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { SurveySchemaInterface } from 'src/interfaces/survey';
|
||||
|
||||
describe('SurveyConfService', () => {
|
||||
let service: SurveyConfService;
|
||||
let surveyConfRepository: MongoRepository<SurveyConf>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SurveyConfService,
|
||||
{
|
||||
provide: getRepositoryToken(SurveyConf),
|
||||
useValue: {
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
save: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SurveyConfService>(SurveyConfService);
|
||||
surveyConfRepository = module.get<MongoRepository<SurveyConf>>(
|
||||
getRepositoryToken(SurveyConf),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create survey configuration successfully', async () => {
|
||||
const mockSchemaData = {};
|
||||
jest.mock('../utils', () => ({
|
||||
getSchemaBySurveyType: jest.fn().mockResolvedValue(mockSchemaData),
|
||||
}));
|
||||
|
||||
surveyConfRepository.create = jest
|
||||
.fn()
|
||||
.mockReturnValue({ pageId: 'testId', code: mockSchemaData });
|
||||
surveyConfRepository.save = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ id: 1, pageId: 'testId', code: mockSchemaData });
|
||||
|
||||
const result = await service.createSurveyConf({
|
||||
surveyId: 'testId',
|
||||
surveyType: 'normal',
|
||||
createMethod: '',
|
||||
createFrom: '',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ id: 1, pageId: 'testId', code: mockSchemaData });
|
||||
expect(surveyConfRepository.findOne).not.toHaveBeenCalled();
|
||||
expect(surveyConfRepository.create).toHaveBeenCalledTimes(1);
|
||||
expect(surveyConfRepository.save).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw SurveyNotFoundException when survey config not found', async () => {
|
||||
try {
|
||||
await service.getSurveyConfBySurveyId('nonExistingId');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(SurveyNotFoundException);
|
||||
expect(error.message).toBe('问卷配置不存在');
|
||||
}
|
||||
|
||||
expect(surveyConfRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { pageId: 'nonExistingId' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should save survey configuration', async () => {
|
||||
// 准备参数和模拟数据
|
||||
const surveyId = 'someSurveyId';
|
||||
const schema = {
|
||||
dataConf: {
|
||||
dataList: [],
|
||||
},
|
||||
bannerConf: {},
|
||||
submitConf: {},
|
||||
baseConf: {},
|
||||
skinConf: {},
|
||||
} as SurveySchemaInterface;
|
||||
|
||||
jest.spyOn(surveyConfRepository, 'findOne').mockResolvedValue({
|
||||
surveyId: surveyId,
|
||||
code: schema,
|
||||
} as unknown as SurveyConf);
|
||||
|
||||
// 调用待测试的方法
|
||||
await service.saveSurveyConf({ surveyId, schema });
|
||||
|
||||
// 验证save方法被调用了一次,并且传入了正确的参数
|
||||
expect(surveyConfRepository.save).toHaveBeenCalledTimes(1);
|
||||
expect(surveyConfRepository.save).toHaveBeenCalledWith({
|
||||
surveyId: surveyId,
|
||||
code: schema,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw when saving survey configuration with non-existing surveyId', async () => {
|
||||
// 准备参数
|
||||
const surveyId = 'nonExistingSurveyId';
|
||||
const schema = {
|
||||
dataConf: {
|
||||
dataList: [],
|
||||
},
|
||||
bannerConf: {},
|
||||
submitConf: {},
|
||||
baseConf: {},
|
||||
skinConf: {},
|
||||
} as SurveySchemaInterface;
|
||||
|
||||
// 调用待测试的方法并期待抛出异常
|
||||
await expect(service.saveSurveyConf({ surveyId, schema })).rejects.toThrow(
|
||||
SurveyNotFoundException,
|
||||
);
|
||||
|
||||
// 验证save方法没有被调用,因为没有找到对应的surveyId
|
||||
expect(surveyConfRepository.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// getSurveyContentByCode方法的单元测试
|
||||
it('should get survey content by code', async () => {
|
||||
// 准备参数和模拟数据
|
||||
const schema = {
|
||||
dataConf: {
|
||||
dataList: [
|
||||
{
|
||||
title: 'Title1',
|
||||
options: [{ text: 'Option1' }, { text: 'Option2' }],
|
||||
},
|
||||
{
|
||||
title: 'Title2',
|
||||
},
|
||||
],
|
||||
},
|
||||
bannerConf: {},
|
||||
submitConf: {},
|
||||
baseConf: {},
|
||||
skinConf: {},
|
||||
} as SurveySchemaInterface;
|
||||
|
||||
// 调用待测试的方法
|
||||
const result = await service.getSurveyContentByCode(schema);
|
||||
|
||||
// 验证返回结果是否正确
|
||||
expect(result).toEqual({
|
||||
text: 'Title1\nOption1\nOption2\nTitle2',
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,74 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SurveyHistoryController } from '../controllers/surveyHistory.controller';
|
||||
import { SurveyHistoryService } from '../services/surveyHistory.service';
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { Authtication } from 'src/guards/authtication';
|
||||
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
describe('SurveyHistoryController', () => {
|
||||
let controller: SurveyHistoryController;
|
||||
let surveyHistoryService: SurveyHistoryService;
|
||||
let surveyMetaService: SurveyMetaService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [SurveyHistoryController],
|
||||
providers: [
|
||||
ConfigService,
|
||||
{
|
||||
provide: SurveyHistoryService,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
getHistoryList: jest.fn().mockResolvedValue('mockHistoryList'),
|
||||
})),
|
||||
},
|
||||
{
|
||||
provide: SurveyMetaService,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
checkSurveyAccess: jest.fn().mockResolvedValue({}),
|
||||
})),
|
||||
},
|
||||
{
|
||||
provide: Authtication,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
canActivate: () => true,
|
||||
})),
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
getUserByUsername() {
|
||||
return {};
|
||||
},
|
||||
})),
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<SurveyHistoryController>(SurveyHistoryController);
|
||||
surveyHistoryService =
|
||||
module.get<SurveyHistoryService>(SurveyHistoryService);
|
||||
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
|
||||
});
|
||||
|
||||
it('should return history list when query is valid', async () => {
|
||||
const req = { user: { username: 'testUser' } };
|
||||
const queryInfo = { surveyId: 'survey123', historyType: 'published' };
|
||||
|
||||
await controller.getList(queryInfo, req);
|
||||
|
||||
expect(surveyMetaService.checkSurveyAccess).toHaveBeenCalledWith({
|
||||
surveyId: queryInfo.surveyId,
|
||||
username: req.user.username,
|
||||
});
|
||||
|
||||
expect(surveyHistoryService.getHistoryList).toHaveBeenCalledWith({
|
||||
surveyId: queryInfo.surveyId,
|
||||
historyType: queryInfo.historyType,
|
||||
});
|
||||
|
||||
expect(surveyHistoryService.getHistoryList).toHaveBeenCalledTimes(1);
|
||||
expect(surveyMetaService.checkSurveyAccess).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
121
server/src/modules/survey/__test/surveyHistory.service.spec.ts
Normal file
121
server/src/modules/survey/__test/surveyHistory.service.spec.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SurveyHistoryService } from '../services/surveyHistory.service';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { SurveyHistory } from 'src/models/surveyHistory.entity';
|
||||
import { HISTORY_TYPE } from 'src/enums';
|
||||
import { SurveySchemaInterface } from 'src/interfaces/survey';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
describe('SurveyHistoryService', () => {
|
||||
let service: SurveyHistoryService;
|
||||
let repository: MongoRepository<SurveyHistory>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SurveyHistoryService,
|
||||
{
|
||||
provide: getRepositoryToken(SurveyHistory),
|
||||
useClass: MongoRepository,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SurveyHistoryService>(SurveyHistoryService);
|
||||
repository = module.get<MongoRepository<SurveyHistory>>(
|
||||
getRepositoryToken(SurveyHistory),
|
||||
);
|
||||
});
|
||||
|
||||
const mockSchema: SurveySchemaInterface = {
|
||||
bannerConf: {
|
||||
titleConfig: undefined,
|
||||
bannerConfig: undefined,
|
||||
},
|
||||
dataConf: {
|
||||
dataList: [],
|
||||
},
|
||||
submitConf: {
|
||||
submitTitle: '',
|
||||
confirmAgain: undefined,
|
||||
msgContent: undefined,
|
||||
},
|
||||
baseConf: {
|
||||
begTime: '',
|
||||
endTime: '',
|
||||
answerBegTime: '',
|
||||
answerEndTime: '',
|
||||
tLimit: 0,
|
||||
language: '',
|
||||
},
|
||||
skinConf: undefined,
|
||||
bottomConf: undefined,
|
||||
};
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('addHistory', () => {
|
||||
it('should add a new history record', async () => {
|
||||
const surveyId = 'survey_id';
|
||||
const schema = mockSchema;
|
||||
const type = HISTORY_TYPE.DAILY_HIS;
|
||||
const user = { _id: 'user_id', username: 'test_user' };
|
||||
|
||||
const spyCreate = jest.spyOn(repository, 'create').mockReturnValueOnce({
|
||||
pageId: surveyId,
|
||||
type,
|
||||
schema,
|
||||
operator: {
|
||||
_id: user._id.toString(),
|
||||
username: user.username,
|
||||
},
|
||||
} as SurveyHistory);
|
||||
|
||||
const spySave = jest
|
||||
.spyOn(repository, 'save')
|
||||
.mockResolvedValueOnce({} as SurveyHistory);
|
||||
|
||||
await service.addHistory({ surveyId, schema, type, user });
|
||||
|
||||
expect(spyCreate).toHaveBeenCalledWith({
|
||||
pageId: surveyId,
|
||||
type,
|
||||
schema,
|
||||
operator: {
|
||||
_id: user._id.toString(),
|
||||
username: user.username,
|
||||
},
|
||||
});
|
||||
expect(spySave).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHistoryList', () => {
|
||||
it('should return a list of history records for a survey', async () => {
|
||||
const surveyId = new ObjectId().toString();
|
||||
const historyType = HISTORY_TYPE.DAILY_HIS;
|
||||
const mockHistory = new SurveyHistory();
|
||||
mockHistory.schema = mockSchema;
|
||||
mockHistory.pageId = surveyId;
|
||||
const expectedResult = [mockHistory];
|
||||
|
||||
const spyFind = jest
|
||||
.spyOn(repository, 'find')
|
||||
.mockResolvedValueOnce(expectedResult);
|
||||
|
||||
const result = await service.getHistoryList({ surveyId, historyType });
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
expect(spyFind).toHaveBeenCalledWith({
|
||||
where: {
|
||||
pageId: surveyId,
|
||||
type: historyType,
|
||||
},
|
||||
take: 100,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
200
server/src/modules/survey/__test/surveyMeta.controller.spec.ts
Normal file
200
server/src/modules/survey/__test/surveyMeta.controller.spec.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SurveyMetaController } from '../controllers/surveyMeta.controller';
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
import { Authtication } from 'src/guards/authtication';
|
||||
import * as Joi from 'joi';
|
||||
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||
|
||||
describe('SurveyMetaController', () => {
|
||||
let controller: SurveyMetaController;
|
||||
let surveyMetaService: SurveyMetaService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [SurveyMetaController],
|
||||
providers: [
|
||||
{
|
||||
provide: SurveyMetaService,
|
||||
useValue: {
|
||||
checkSurveyAccess: jest.fn().mockResolvedValue({}),
|
||||
editSurveyMeta: jest.fn().mockResolvedValue(undefined),
|
||||
getSurveyMetaList: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ count: 0, data: [] }),
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideGuard(Authtication)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
controller = module.get<SurveyMetaController>(SurveyMetaController);
|
||||
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
|
||||
});
|
||||
|
||||
it('should update survey meta', async () => {
|
||||
const reqBody = {
|
||||
remark: 'Test remark',
|
||||
title: 'Test title',
|
||||
surveyId: 'test-survey-id',
|
||||
};
|
||||
const req = {
|
||||
user: {
|
||||
username: 'test-user',
|
||||
},
|
||||
};
|
||||
|
||||
const survey = {
|
||||
title: '',
|
||||
remark: '',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockImplementation(() => {
|
||||
return Promise.resolve(survey) as Promise<SurveyMeta>;
|
||||
});
|
||||
|
||||
const result = await controller.updateMeta(reqBody, req);
|
||||
|
||||
expect(surveyMetaService.checkSurveyAccess).toHaveBeenCalledWith({
|
||||
surveyId: reqBody.surveyId,
|
||||
username: req.user.username,
|
||||
});
|
||||
|
||||
expect(surveyMetaService.editSurveyMeta).toHaveBeenCalledWith({
|
||||
title: reqBody.title,
|
||||
remark: reqBody.remark,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ code: 200 });
|
||||
});
|
||||
|
||||
it('should validate request body with Joi', async () => {
|
||||
const reqBody = {
|
||||
// Missing title and surveyId
|
||||
};
|
||||
const req = {
|
||||
user: {
|
||||
username: 'test-user',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await controller.updateMeta(reqBody, req);
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Joi.ValidationError);
|
||||
expect(error.details[0].message).toMatch('"title" is required');
|
||||
}
|
||||
|
||||
expect(surveyMetaService.checkSurveyAccess).not.toHaveBeenCalled();
|
||||
expect(surveyMetaService.editSurveyMeta).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should get survey meta list', async () => {
|
||||
const queryInfo = {
|
||||
curPage: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
const req = {
|
||||
user: {
|
||||
username: 'test-user',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'getSurveyMetaList')
|
||||
.mockImplementation(() => {
|
||||
const date = new Date().getTime();
|
||||
return Promise.resolve({
|
||||
count: 10,
|
||||
data: [
|
||||
{
|
||||
id: '1',
|
||||
createDate: date,
|
||||
updateDate: date,
|
||||
curStatus: {
|
||||
date: date,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const result = await controller.getList(queryInfo, req);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
data: {
|
||||
count: 10,
|
||||
data: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
createDate: expect.stringMatching(
|
||||
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
|
||||
),
|
||||
updateDate: expect.stringMatching(
|
||||
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
|
||||
),
|
||||
curStatus: expect.objectContaining({
|
||||
date: expect.stringMatching(
|
||||
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
|
||||
),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
expect(surveyMetaService.getSurveyMetaList).toHaveBeenCalledWith({
|
||||
pageNum: queryInfo.curPage,
|
||||
pageSize: queryInfo.pageSize,
|
||||
username: req.user.username,
|
||||
filter: {},
|
||||
order: {},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
it('should get survey meta list with filter and order', async () => {
|
||||
const queryInfo = {
|
||||
curPage: 1,
|
||||
pageSize: 10,
|
||||
filter: JSON.stringify([
|
||||
{
|
||||
comparator: '',
|
||||
condition: [{ field: 'title', value: 'hahah', comparator: '$regex' }],
|
||||
},
|
||||
{
|
||||
comparator: '',
|
||||
condition: [{ field: 'surveyType', value: 'normal' }],
|
||||
},
|
||||
]),
|
||||
order: JSON.stringify([{ field: 'createDate', value: -1 }]),
|
||||
};
|
||||
const req = {
|
||||
user: {
|
||||
username: 'test-user',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await controller.getList(queryInfo, req);
|
||||
|
||||
expect(result.code).toEqual(200);
|
||||
expect(surveyMetaService.getSurveyMetaList).toHaveBeenCalledWith({
|
||||
pageNum: queryInfo.curPage,
|
||||
pageSize: queryInfo.pageSize,
|
||||
username: req.user.username,
|
||||
filter: { surveyType: 'normal', title: { $regex: 'hahah' } },
|
||||
order: { createDate: -1 },
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
});
|
264
server/src/modules/survey/__test/surveyMeta.service.spec.ts
Normal file
264
server/src/modules/survey/__test/surveyMeta.service.spec.ts
Normal file
@ -0,0 +1,264 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
import { NoSurveyPermissionException } from 'src/exceptions/noSurveyPermissionException';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { SurveyUtilPlugin } from 'src/securityPlugin/surveyUtilPlugin';
|
||||
|
||||
describe('SurveyMetaService', () => {
|
||||
let service: SurveyMetaService;
|
||||
let surveyRepository: MongoRepository<SurveyMeta>;
|
||||
let pluginManager: XiaojuSurveyPluginManager;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SurveyMetaService,
|
||||
{
|
||||
provide: getRepositoryToken(SurveyMeta),
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
count: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
findAndCount: jest.fn(),
|
||||
},
|
||||
},
|
||||
PluginManagerProvider,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SurveyMetaService>(SurveyMetaService);
|
||||
surveyRepository = module.get<MongoRepository<SurveyMeta>>(
|
||||
getRepositoryToken(SurveyMeta),
|
||||
);
|
||||
pluginManager = module.get<XiaojuSurveyPluginManager>(
|
||||
XiaojuSurveyPluginManager,
|
||||
);
|
||||
pluginManager.registerPlugin(new SurveyUtilPlugin());
|
||||
});
|
||||
|
||||
describe('getNewSurveyPath', () => {
|
||||
it('should generate a new survey path', async () => {
|
||||
jest.spyOn(surveyRepository, 'count').mockResolvedValueOnce(1);
|
||||
jest.spyOn(surveyRepository, 'count').mockResolvedValueOnce(0);
|
||||
|
||||
const surveyPath = await service.getNewSurveyPath();
|
||||
|
||||
expect(typeof surveyPath).toBe('string');
|
||||
expect(surveyRepository.count).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkSurveyAccess', () => {
|
||||
it('should return survey when user has access', async () => {
|
||||
const surveyId = new ObjectId().toHexString();
|
||||
const username = 'testUser';
|
||||
const survey = { owner: username } as SurveyMeta;
|
||||
jest.spyOn(surveyRepository, 'findOne').mockResolvedValue(survey);
|
||||
|
||||
const result = await service.checkSurveyAccess({ surveyId, username });
|
||||
|
||||
expect(result).toBe(survey);
|
||||
expect(surveyRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { _id: new ObjectId(surveyId) },
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw SurveyNotFoundException when survey not found', async () => {
|
||||
const surveyId = new ObjectId().toHexString();
|
||||
const username = 'testUser';
|
||||
jest.spyOn(surveyRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.checkSurveyAccess({ surveyId, username }),
|
||||
).rejects.toThrow(SurveyNotFoundException);
|
||||
|
||||
expect(surveyRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { _id: new ObjectId(surveyId) },
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw NoSurveyPermissionException when user has no access', async () => {
|
||||
const surveyId = new ObjectId().toHexString();
|
||||
const username = 'testUser';
|
||||
const surveyOwner = 'otherUser';
|
||||
const survey = { owner: surveyOwner } as SurveyMeta;
|
||||
jest.spyOn(surveyRepository, 'findOne').mockResolvedValue(survey);
|
||||
|
||||
await expect(
|
||||
service.checkSurveyAccess({ surveyId, username }),
|
||||
).rejects.toThrow(NoSurveyPermissionException);
|
||||
|
||||
expect(surveyRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { _id: new ObjectId(surveyId) },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSurveyMeta', () => {
|
||||
it('should create a new survey meta and return it', async () => {
|
||||
const params = {
|
||||
title: 'Test Survey',
|
||||
remark: 'This is a test survey',
|
||||
surveyType: 'normal',
|
||||
username: 'testUser',
|
||||
createMethod: '',
|
||||
createFrom: '',
|
||||
};
|
||||
const newSurvey = new SurveyMeta();
|
||||
|
||||
const mockedSurveyPath = 'mockedSurveyPath';
|
||||
jest
|
||||
.spyOn(service, 'getNewSurveyPath')
|
||||
.mockResolvedValue(mockedSurveyPath);
|
||||
|
||||
jest
|
||||
.spyOn(surveyRepository, 'create')
|
||||
.mockImplementation(() => newSurvey);
|
||||
jest.spyOn(surveyRepository, 'save').mockResolvedValue(newSurvey);
|
||||
|
||||
const result = await service.createSurveyMeta(params);
|
||||
|
||||
expect(surveyRepository.create).toHaveBeenCalledWith({
|
||||
title: params.title,
|
||||
remark: params.remark,
|
||||
surveyType: params.surveyType,
|
||||
surveyPath: mockedSurveyPath,
|
||||
creator: params.username,
|
||||
owner: params.username,
|
||||
createMethod: params.createMethod,
|
||||
createFrom: params.createFrom,
|
||||
});
|
||||
expect(surveyRepository.save).toHaveBeenCalledWith(newSurvey);
|
||||
expect(result).toEqual(newSurvey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('editSurveyMeta', () => {
|
||||
it('should edit a survey meta and return it if in NEW or EDITING status', async () => {
|
||||
const survey = new SurveyMeta();
|
||||
survey.curStatus = { status: RECORD_STATUS.PUBLISHED, date: Date.now() };
|
||||
survey.statusList = [];
|
||||
jest.spyOn(surveyRepository, 'save').mockResolvedValue(survey);
|
||||
|
||||
const result = await service.editSurveyMeta(survey);
|
||||
|
||||
expect(survey.curStatus.status).toEqual(RECORD_STATUS.EDITING);
|
||||
expect(survey.statusList.length).toBe(1);
|
||||
expect(survey.statusList[0].status).toEqual(RECORD_STATUS.EDITING);
|
||||
expect(surveyRepository.save).toHaveBeenCalledWith(survey);
|
||||
expect(result).toEqual(survey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteSurveyMeta', () => {
|
||||
it('should delete survey meta and update status', async () => {
|
||||
// 准备假的SurveyMeta对象
|
||||
const survey = new SurveyMeta();
|
||||
survey.curStatus = { status: RECORD_STATUS.NEW, date: Date.now() };
|
||||
survey.statusList = [];
|
||||
|
||||
// 模拟save方法
|
||||
jest.spyOn(surveyRepository, 'save').mockResolvedValue(survey);
|
||||
|
||||
// 调用要测试的方法
|
||||
const result = await service.deleteSurveyMeta(survey);
|
||||
|
||||
// 验证结果
|
||||
expect(result).toBe(survey);
|
||||
expect(survey.curStatus.status).toBe(RECORD_STATUS.REMOVED);
|
||||
expect(survey.statusList.length).toBe(1);
|
||||
expect(survey.statusList[0].status).toBe(RECORD_STATUS.REMOVED);
|
||||
expect(surveyRepository.save).toHaveBeenCalledTimes(1);
|
||||
expect(surveyRepository.save).toHaveBeenCalledWith(survey);
|
||||
});
|
||||
|
||||
it('should throw exception when survey is already removed', async () => {
|
||||
// 准备假的SurveyMeta对象,其状态已设置为REMOVED
|
||||
const survey = new SurveyMeta();
|
||||
survey.curStatus = { status: RECORD_STATUS.REMOVED, date: Date.now() };
|
||||
|
||||
// 调用要测试的方法并期待异常
|
||||
await expect(service.deleteSurveyMeta(survey)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
|
||||
// 验证save方法没有被调用
|
||||
expect(surveyRepository.save).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSurveyMetaList', () => {
|
||||
it('should return a list of survey metadata', async () => {
|
||||
// 准备模拟数据
|
||||
const mockData = [
|
||||
{ _id: 1, title: 'Survey 1' },
|
||||
{ _id: 2, title: 'Survey 2' },
|
||||
] as unknown as Array<SurveyMeta>;
|
||||
const mockCount = 2;
|
||||
|
||||
jest
|
||||
.spyOn(surveyRepository, 'findAndCount')
|
||||
.mockResolvedValue([mockData, mockCount]);
|
||||
|
||||
// 调用方法并检查返回值
|
||||
const condition = {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
username: 'testUser',
|
||||
filter: {},
|
||||
order: {},
|
||||
};
|
||||
const result = await service.getSurveyMetaList(condition);
|
||||
|
||||
// 验证返回值
|
||||
expect(result).toEqual({ data: mockData, count: mockCount });
|
||||
// 验证repository方法被正确调用
|
||||
expect(surveyRepository.findAndCount).toHaveBeenCalledWith({
|
||||
where: {
|
||||
owner: 'testUser',
|
||||
'curStatus.status': { $ne: 'removed' },
|
||||
},
|
||||
skip: 0,
|
||||
take: 10,
|
||||
order: { createDate: -1 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('publishSurveyMeta', () => {
|
||||
it('should publish a survey meta and add status to statusList', async () => {
|
||||
// 准备模拟数据
|
||||
const surveyMeta = {
|
||||
id: 1,
|
||||
title: 'Test Survey',
|
||||
statusList: [],
|
||||
} as unknown as SurveyMeta;
|
||||
const savedSurveyMeta = {
|
||||
...surveyMeta,
|
||||
curStatus: {
|
||||
status: RECORD_STATUS.PUBLISHED,
|
||||
date: expect.any(Number),
|
||||
},
|
||||
} as unknown as SurveyMeta;
|
||||
|
||||
jest.spyOn(surveyRepository, 'save').mockResolvedValue(savedSurveyMeta);
|
||||
|
||||
// 调用方法并检查返回值
|
||||
const result = await service.publishSurveyMeta({ surveyMeta });
|
||||
|
||||
// 验证返回值
|
||||
expect(result).toEqual(savedSurveyMeta);
|
||||
// 验证repository方法被正确调用
|
||||
expect(surveyRepository.save).toHaveBeenCalledWith(savedSurveyMeta);
|
||||
});
|
||||
});
|
||||
});
|
32
server/src/modules/survey/__test/surveyUI.controller.spec.ts
Normal file
32
server/src/modules/survey/__test/surveyUI.controller.spec.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SurveyUIController } from '../controllers/surveyUI.controller';
|
||||
import { Response } from 'express';
|
||||
import { join } from 'path';
|
||||
|
||||
describe('SurveyUIController', () => {
|
||||
let controller: SurveyUIController;
|
||||
let res: Response;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [SurveyUIController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<SurveyUIController>(SurveyUIController);
|
||||
res = {
|
||||
sendFile: jest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as Response;
|
||||
});
|
||||
|
||||
it('should send the correct file for the home route', () => {
|
||||
controller.home(res);
|
||||
const expectedPath = join(process.cwd(), 'public', 'management.html');
|
||||
expect(res.sendFile).toHaveBeenCalledWith(expectedPath);
|
||||
});
|
||||
|
||||
it('should send the correct file for the management route', () => {
|
||||
controller.management(res);
|
||||
const expectedPath = join(process.cwd(), 'public', 'management.html');
|
||||
expect(res.sendFile).toHaveBeenCalledWith(expectedPath);
|
||||
});
|
||||
});
|
@ -15,21 +15,7 @@ import * as Joi from 'joi';
|
||||
import { Authtication } from 'src/guards/authtication';
|
||||
import moment from 'moment';
|
||||
|
||||
type FilterItem = {
|
||||
comparator?: string;
|
||||
condition: Array<FilterCondition>;
|
||||
};
|
||||
|
||||
type FilterCondition = {
|
||||
field: string;
|
||||
comparator?: string;
|
||||
value: string & Array<FilterItem>;
|
||||
};
|
||||
|
||||
type OrderItem = {
|
||||
field: string;
|
||||
value: number;
|
||||
};
|
||||
import { getFilter, getOrder } from 'src/utils/surveyUtil';
|
||||
|
||||
@Controller('/api/survey')
|
||||
export class SurveyMetaController {
|
||||
@ -83,7 +69,7 @@ export class SurveyMetaController {
|
||||
order = {};
|
||||
if (validationResult.filter) {
|
||||
try {
|
||||
filter = this.getFilter(
|
||||
filter = getFilter(
|
||||
JSON.parse(decodeURIComponent(validationResult.filter)),
|
||||
);
|
||||
} catch (error) {
|
||||
@ -92,7 +78,7 @@ export class SurveyMetaController {
|
||||
}
|
||||
if (validationResult.order) {
|
||||
try {
|
||||
order = order = this.getOrder(
|
||||
order = order = getOrder(
|
||||
JSON.parse(decodeURIComponent(validationResult.order)),
|
||||
);
|
||||
} catch (error) {
|
||||
@ -124,62 +110,4 @@ export class SurveyMetaController {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private getFilter(filterList: Array<FilterItem>) {
|
||||
const allowFilterField = [
|
||||
'title',
|
||||
'remark',
|
||||
'surveyType',
|
||||
'curStatus.status',
|
||||
];
|
||||
return filterList.reduce(
|
||||
(preItem, curItem) => {
|
||||
const condition = curItem.condition
|
||||
.filter((item) => allowFilterField.includes(item.field))
|
||||
.reduce((pre, cur) => {
|
||||
switch (cur.comparator) {
|
||||
case '$ne':
|
||||
pre[cur.field] = {
|
||||
$ne: cur.value,
|
||||
};
|
||||
break;
|
||||
case '$regex':
|
||||
pre[cur.field] = {
|
||||
$regex: cur.value,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
pre[cur.field] = cur.value;
|
||||
break;
|
||||
}
|
||||
return pre;
|
||||
}, {});
|
||||
switch (curItem.comparator) {
|
||||
case '$or':
|
||||
if (!Array.isArray(preItem.$or)) {
|
||||
preItem.$or = [];
|
||||
}
|
||||
preItem.$or.push(condition);
|
||||
break;
|
||||
default:
|
||||
Object.assign(preItem, condition);
|
||||
break;
|
||||
}
|
||||
return preItem;
|
||||
},
|
||||
{} as { $or?: Array<Record<string, string>> } & Record<string, string>,
|
||||
);
|
||||
}
|
||||
|
||||
private getOrder(order: Array<OrderItem>) {
|
||||
const allowOrderFields = ['createDate', 'updateDate', 'curStatus.date'];
|
||||
|
||||
const orderList = order.filter((orderItem) =>
|
||||
allowOrderFields.includes(orderItem.field),
|
||||
);
|
||||
return orderList.reduce((pre, cur) => {
|
||||
pre[cur.field] = cur.value === 1 ? 1 : -1;
|
||||
return pre;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Controller, Get, Param, Res } from '@nestjs/common';
|
||||
import { Controller, Get, Res } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { join } from 'path';
|
||||
|
||||
@ -11,8 +11,8 @@ export class SurveyUIController {
|
||||
res.sendFile(join(process.cwd(), 'public', 'management.html'));
|
||||
}
|
||||
|
||||
@Get('/management/:surveyId')
|
||||
management(@Param('surveyId') surveyId: string, @Res() res: Response) {
|
||||
@Get('/management/:path*')
|
||||
management(@Res() res: Response) {
|
||||
res.sendFile(join(process.cwd(), 'public', 'management.html'));
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import moment from 'moment';
|
||||
import { keyBy } from 'lodash';
|
||||
import { DataItem } from 'src/interfaces/survey';
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
|
||||
import { getListHeadByDataList } from '../utils';
|
||||
@Injectable()
|
||||
export class DataStatisticService {
|
||||
constructor(
|
||||
@ -15,46 +15,6 @@ export class DataStatisticService {
|
||||
private readonly surveyResponseRepository: MongoRepository<SurveyResponse>,
|
||||
) {}
|
||||
|
||||
private getListHeadByDataList(dataList) {
|
||||
const listHead = dataList.map((question) => {
|
||||
let othersCode;
|
||||
if (question.type === 'radio-star') {
|
||||
const rangeConfigKeys = Object.keys(question.rangeConfig);
|
||||
if (rangeConfigKeys.length > 0) {
|
||||
othersCode = [
|
||||
{ code: `${question.field}_custom`, option: '填写理由' },
|
||||
];
|
||||
}
|
||||
} else {
|
||||
othersCode = (question.options || [])
|
||||
.filter((optionItem) => optionItem.othersKey)
|
||||
.map((optionItem) => {
|
||||
return {
|
||||
code: optionItem.othersKey,
|
||||
option: optionItem.text,
|
||||
};
|
||||
});
|
||||
}
|
||||
return {
|
||||
field: question.field,
|
||||
title: question.title,
|
||||
type: question.type,
|
||||
othersCode,
|
||||
};
|
||||
});
|
||||
listHead.push({
|
||||
field: 'difTime',
|
||||
title: '答题耗时(秒)',
|
||||
type: 'text',
|
||||
});
|
||||
listHead.push({
|
||||
field: 'createDate',
|
||||
title: '提交时间',
|
||||
type: 'text',
|
||||
});
|
||||
return listHead;
|
||||
}
|
||||
|
||||
async getDataTable({
|
||||
surveyId,
|
||||
pageNum,
|
||||
@ -67,7 +27,7 @@ export class DataStatisticService {
|
||||
responseSchema: ResponseSchema;
|
||||
}) {
|
||||
const dataList = responseSchema?.code?.dataConf?.dataList || [];
|
||||
const listHead = this.getListHeadByDataList(dataList);
|
||||
const listHead = getListHeadByDataList(dataList);
|
||||
const dataListMap = keyBy(dataList, 'field');
|
||||
const where = {
|
||||
pageId: surveyId,
|
||||
|
@ -2,25 +2,11 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { SurveyConf } from 'src/models/surveyConf.entity';
|
||||
import templateBase from '../template/surveyTemplate/templateBase.json';
|
||||
import normalCode from '../template/surveyTemplate/survey/normal.json';
|
||||
import npsCode from '../template/surveyTemplate/survey/nps.json';
|
||||
import registerCode from '../template/surveyTemplate/survey/register.json';
|
||||
import voteCode from '../template/surveyTemplate/survey/vote.json';
|
||||
import { get } from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { SurveySchemaInterface } from 'src/interfaces/survey';
|
||||
|
||||
const schemaDataMap = {
|
||||
normal: normalCode,
|
||||
nps: npsCode,
|
||||
register: registerCode,
|
||||
vote: voteCode,
|
||||
};
|
||||
import { getSchemaBySurveyType } from '../utils';
|
||||
|
||||
@Injectable()
|
||||
export class SurveyConfService {
|
||||
@ -29,24 +15,6 @@ export class SurveyConfService {
|
||||
private readonly surveyConfRepository: MongoRepository<SurveyConf>,
|
||||
) {}
|
||||
|
||||
private async getSchemaBySurveyType(surveyType: string) {
|
||||
// Implement your logic here
|
||||
const codeData = get(schemaDataMap, surveyType);
|
||||
if (!codeData) {
|
||||
throw new HttpException(
|
||||
'问卷类型不存在',
|
||||
EXCEPTION_CODE.SURVEY_TYPE_ERROR,
|
||||
);
|
||||
}
|
||||
const code = Object.assign({}, templateBase, codeData);
|
||||
const nowMoment = moment();
|
||||
code.baseConf.begTime = nowMoment.format('YYYY-MM-DD HH:mm:ss');
|
||||
code.baseConf.endTime = nowMoment
|
||||
.add(10, 'years')
|
||||
.format('YYYY-MM-DD HH:mm:ss');
|
||||
return code;
|
||||
}
|
||||
|
||||
async createSurveyConf(params: {
|
||||
surveyId: string;
|
||||
surveyType: string;
|
||||
@ -59,7 +27,14 @@ export class SurveyConfService {
|
||||
const codeInfo = await this.getSurveyConfBySurveyId(createFrom);
|
||||
schemaData = codeInfo.code;
|
||||
} else {
|
||||
schemaData = await this.getSchemaBySurveyType(surveyType);
|
||||
try {
|
||||
schemaData = await getSchemaBySurveyType(surveyType);
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
error.message,
|
||||
EXCEPTION_CODE.SURVEY_TYPE_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const newCode = this.surveyConfRepository.create({
|
||||
|
@ -18,8 +18,8 @@ export class SurveyMetaService {
|
||||
private readonly pluginManager: XiaojuSurveyPluginManager,
|
||||
) {}
|
||||
|
||||
private async getNewSurveyPath(): Promise<string> {
|
||||
let surveyPath = this.pluginManager.triggerHook('genSurveyPath');
|
||||
async getNewSurveyPath(): Promise<string> {
|
||||
let surveyPath = await this.pluginManager.triggerHook('genSurveyPath');
|
||||
while (true) {
|
||||
const count = await this.surveyRepository.count({
|
||||
where: {
|
||||
@ -29,7 +29,7 @@ export class SurveyMetaService {
|
||||
if (count === 0) {
|
||||
break;
|
||||
}
|
||||
surveyPath = this.pluginManager.triggerHook('genSurveyPath');
|
||||
surveyPath = await this.pluginManager.triggerHook('genSurveyPath');
|
||||
}
|
||||
return surveyPath;
|
||||
}
|
||||
|
67
server/src/modules/survey/utils/index.ts
Normal file
67
server/src/modules/survey/utils/index.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { get } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import templateBase from '../template/surveyTemplate/templateBase.json';
|
||||
import normalCode from '../template/surveyTemplate/survey/normal.json';
|
||||
import npsCode from '../template/surveyTemplate/survey/nps.json';
|
||||
import registerCode from '../template/surveyTemplate/survey/register.json';
|
||||
import voteCode from '../template/surveyTemplate/survey/vote.json';
|
||||
|
||||
const schemaDataMap = {
|
||||
normal: normalCode,
|
||||
nps: npsCode,
|
||||
register: registerCode,
|
||||
vote: voteCode,
|
||||
};
|
||||
|
||||
export async function getSchemaBySurveyType(surveyType: string) {
|
||||
// Implement your logic here
|
||||
const codeData = get(schemaDataMap, surveyType);
|
||||
if (!codeData) {
|
||||
throw new Error('问卷类型不存在');
|
||||
}
|
||||
const code = Object.assign({}, templateBase, codeData);
|
||||
const nowMoment = moment();
|
||||
code.baseConf.begTime = nowMoment.format('YYYY-MM-DD HH:mm:ss');
|
||||
code.baseConf.endTime = nowMoment
|
||||
.add(10, 'years')
|
||||
.format('YYYY-MM-DD HH:mm:ss');
|
||||
return code;
|
||||
}
|
||||
|
||||
export function getListHeadByDataList(dataList) {
|
||||
const listHead = dataList.map((question) => {
|
||||
let othersCode;
|
||||
if (question.type === 'radio-star') {
|
||||
const rangeConfigKeys = Object.keys(question.rangeConfig);
|
||||
if (rangeConfigKeys.length > 0) {
|
||||
othersCode = [{ code: `${question.field}_custom`, option: '填写理由' }];
|
||||
}
|
||||
} else {
|
||||
othersCode = (question.options || [])
|
||||
.filter((optionItem) => optionItem.othersKey)
|
||||
.map((optionItem) => {
|
||||
return {
|
||||
code: optionItem.othersKey,
|
||||
option: optionItem.text,
|
||||
};
|
||||
});
|
||||
}
|
||||
return {
|
||||
field: question.field,
|
||||
title: question.title,
|
||||
type: question.type,
|
||||
othersCode,
|
||||
};
|
||||
});
|
||||
listHead.push({
|
||||
field: 'difTime',
|
||||
title: '答题耗时(秒)',
|
||||
type: 'text',
|
||||
});
|
||||
listHead.push({
|
||||
field: 'createDate',
|
||||
title: '提交时间',
|
||||
type: 'text',
|
||||
});
|
||||
return listHead;
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { ClientEncryptService } from '../services/clientEncrypt.service';
|
||||
import { ClientEncrypt } from 'src/models/clientEncrypt.entity';
|
||||
import { ENCRYPT_TYPE } from 'src/enums/encrypt';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
describe('ClientEncryptService', () => {
|
||||
let service: ClientEncryptService;
|
||||
let repository: MongoRepository<ClientEncrypt>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ClientEncryptService,
|
||||
{
|
||||
provide: getRepositoryToken(ClientEncrypt),
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
updateOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ClientEncryptService>(ClientEncryptService);
|
||||
repository = module.get<MongoRepository<ClientEncrypt>>(
|
||||
getRepositoryToken(ClientEncrypt),
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('addAes', () => {
|
||||
it('should save AES encrypt info', async () => {
|
||||
const secretKey = 'my-secret-key';
|
||||
const encryptInfo = {
|
||||
data: { secretKey },
|
||||
type: ENCRYPT_TYPE.AES,
|
||||
} as ClientEncrypt;
|
||||
jest.spyOn(repository, 'create').mockReturnValue(encryptInfo);
|
||||
jest.spyOn(repository, 'save').mockResolvedValue(encryptInfo);
|
||||
|
||||
const result = await service.addAes({ secretKey });
|
||||
|
||||
expect(repository.create).toHaveBeenCalledWith(encryptInfo);
|
||||
expect(repository.save).toHaveBeenCalledWith(encryptInfo);
|
||||
expect(result).toEqual(encryptInfo);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addRsa', () => {
|
||||
it('should save RSA encrypt info', async () => {
|
||||
const publicKey = 'my-public-key';
|
||||
const privateKey = 'my-private-key';
|
||||
const encryptInfo = {
|
||||
data: { publicKey, privateKey },
|
||||
type: ENCRYPT_TYPE.RSA,
|
||||
} as ClientEncrypt;
|
||||
jest.spyOn(repository, 'create').mockReturnValue(encryptInfo);
|
||||
jest.spyOn(repository, 'save').mockResolvedValue(encryptInfo);
|
||||
|
||||
const result = await service.addRsa({ publicKey, privateKey });
|
||||
|
||||
expect(repository.create).toHaveBeenCalledWith(encryptInfo);
|
||||
expect(repository.save).toHaveBeenCalledWith(encryptInfo);
|
||||
expect(result).toEqual(encryptInfo);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEncryptInfoById', () => {
|
||||
it('should return encrypt info by id', async () => {
|
||||
const id = new ObjectId().toHexString();
|
||||
const encryptInfo = {
|
||||
id,
|
||||
type: ENCRYPT_TYPE.AES,
|
||||
} as unknown as ClientEncrypt;
|
||||
jest.spyOn(repository, 'findOne').mockResolvedValue(encryptInfo);
|
||||
|
||||
const result = await service.getEncryptInfoById(id);
|
||||
|
||||
expect(repository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
_id: new ObjectId(id),
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result).toEqual(encryptInfo);
|
||||
});
|
||||
|
||||
it('should return null if encrypt info not found', async () => {
|
||||
const id = new ObjectId().toHexString();
|
||||
jest.spyOn(repository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
const result = await service.getEncryptInfoById(id);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteEncryptInfo', () => {
|
||||
it('should delete encrypt info by id', async () => {
|
||||
const id = new ObjectId().toHexString();
|
||||
const updateResult = { matchedCount: 1, modifiedCount: 1 };
|
||||
jest.spyOn(repository, 'updateOne').mockResolvedValue(updateResult);
|
||||
|
||||
const result = await service.deleteEncryptInfo(id);
|
||||
expect(result).toEqual(updateResult);
|
||||
});
|
||||
});
|
||||
});
|
103
server/src/modules/surveyResponse/__test/counter.service.spec.ts
Normal file
103
server/src/modules/surveyResponse/__test/counter.service.spec.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CounterService } from '../services/counter.service';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { Counter } from 'src/models/counter.entity';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
describe('CounterService', () => {
|
||||
let service: CounterService;
|
||||
let counterRepository: MongoRepository<Counter>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
CounterService,
|
||||
{
|
||||
provide: getRepositoryToken(Counter),
|
||||
useValue: {
|
||||
updateOne: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<CounterService>(CounterService);
|
||||
counterRepository = module.get<MongoRepository<Counter>>(
|
||||
getRepositoryToken(Counter),
|
||||
);
|
||||
});
|
||||
|
||||
it('should update counter data', async () => {
|
||||
const data = { someData: 'someValue' };
|
||||
const updateResult = { rawResponse: { matchedCount: 1, modifiedCount: 1 } };
|
||||
jest.spyOn(counterRepository, 'updateOne').mockResolvedValue(updateResult);
|
||||
|
||||
const result = await service.set({
|
||||
surveyPath: 'testPath',
|
||||
key: 'testKey',
|
||||
data,
|
||||
type: 'testType',
|
||||
});
|
||||
|
||||
expect(result).toEqual(updateResult);
|
||||
expect(counterRepository.updateOne).toHaveBeenCalledWith(
|
||||
{ key: 'testKey', surveyPath: 'testPath', type: 'testType' },
|
||||
{
|
||||
$set: {
|
||||
key: 'testKey',
|
||||
surveyPath: 'testPath',
|
||||
type: 'testType',
|
||||
data,
|
||||
},
|
||||
},
|
||||
{ upsert: true },
|
||||
);
|
||||
});
|
||||
|
||||
it('should get counter data', async () => {
|
||||
const expectedData = { someData: 'someValue' };
|
||||
const counterEntity = new Counter();
|
||||
counterEntity.data = expectedData;
|
||||
jest.spyOn(counterRepository, 'findOne').mockResolvedValue(counterEntity);
|
||||
|
||||
const result = await service.get({
|
||||
surveyPath: 'testPath',
|
||||
key: 'testKey',
|
||||
type: 'testType',
|
||||
});
|
||||
|
||||
expect(result).toEqual(expectedData);
|
||||
expect(counterRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { key: 'testKey', surveyPath: 'testPath', type: 'testType' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should get all counter data', async () => {
|
||||
const expectedData = {
|
||||
key1: { someData: 'value1' },
|
||||
key2: { someData: 'value2' },
|
||||
};
|
||||
const counterEntities = [
|
||||
{ key: 'key1', data: expectedData.key1 },
|
||||
{ key: 'key2', data: expectedData.key2 },
|
||||
] as unknown as Array<Counter>;
|
||||
jest.spyOn(counterRepository, 'find').mockResolvedValue(counterEntities);
|
||||
|
||||
const result = await service.getAll({
|
||||
surveyPath: 'testPath',
|
||||
keyList: ['key1', 'key2'],
|
||||
type: 'testType',
|
||||
});
|
||||
|
||||
expect(result).toEqual(expectedData);
|
||||
expect(counterRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
key: { $in: ['key1', 'key2'] },
|
||||
surveyPath: 'testPath',
|
||||
type: 'testType',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
239
server/src/modules/surveyResponse/__test/mockResponseSchema.ts
Normal file
239
server/src/modules/surveyResponse/__test/mockResponseSchema.ts
Normal file
@ -0,0 +1,239 @@
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
export const mockResponseSchema: ResponseSchema = {
|
||||
_id: new ObjectId('65f29f8892862d6a9067ad25'),
|
||||
curStatus: {
|
||||
status: RECORD_STATUS.PUBLISHED,
|
||||
date: 1710399368439,
|
||||
},
|
||||
statusList: [
|
||||
{
|
||||
status: RECORD_STATUS.PUBLISHED,
|
||||
date: 1710399368439,
|
||||
},
|
||||
],
|
||||
createDate: 1710399368440,
|
||||
updateDate: 1710399368440,
|
||||
title: '加密全流程',
|
||||
surveyPath: 'EBzdmnSp',
|
||||
code: {
|
||||
bannerConf: {
|
||||
titleConfig: {
|
||||
mainTitle:
|
||||
'<h3 style="text-align: center">欢迎填写问卷</h3><p>为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,<span style="color: rgb(204, 0, 0)">期待您的参与!</span></p>',
|
||||
subTitle: '',
|
||||
},
|
||||
bannerConfig: {
|
||||
bgImage: '/imgs/skin/17e06b7604a007e1d3e1453b9ddadc3c.webp',
|
||||
videoLink: '',
|
||||
postImg: '',
|
||||
},
|
||||
},
|
||||
baseConf: {
|
||||
begTime: '2024-03-14 14:54:41',
|
||||
endTime: '2034-03-14 14:54:41',
|
||||
language: 'chinese',
|
||||
tLimit: 10,
|
||||
answerBegTime: '',
|
||||
answerEndTime: '',
|
||||
},
|
||||
bottomConf: {
|
||||
logoImage: '/imgs/Logo.webp',
|
||||
logoImageWidth: '60%',
|
||||
},
|
||||
skinConf: {
|
||||
skinColor: '#4a4c5b',
|
||||
inputBgColor: '#ffffff',
|
||||
},
|
||||
submitConf: {
|
||||
submitTitle: '提交',
|
||||
msgContent: {
|
||||
msg_200: '提交成功',
|
||||
msg_9001: '您来晚了,感谢支持问卷~',
|
||||
msg_9002: '请勿多次提交!',
|
||||
msg_9003: '您来晚了,已经满额!',
|
||||
msg_9004: '提交失败!',
|
||||
},
|
||||
confirmAgain: {
|
||||
is_again: true,
|
||||
again_text: '确认要提交吗?',
|
||||
},
|
||||
},
|
||||
dataConf: {
|
||||
dataList: [
|
||||
{
|
||||
isRequired: true,
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'text',
|
||||
valid: '',
|
||||
field: 'data458',
|
||||
title: '<p>您的手机号</p>',
|
||||
placeholder: '',
|
||||
randomSort: false,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
star: 5,
|
||||
nps: {
|
||||
leftText: '极不满意',
|
||||
rightText: '极满意',
|
||||
},
|
||||
placeholderDesc: '',
|
||||
textRange: {
|
||||
min: {
|
||||
placeholder: '0',
|
||||
value: 0,
|
||||
},
|
||||
max: {
|
||||
placeholder: '500',
|
||||
value: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
isRequired: true,
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'radio',
|
||||
placeholderDesc: '',
|
||||
field: 'data515',
|
||||
title: '<p>您的性别</p>',
|
||||
placeholder: '',
|
||||
randomSort: false,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
options: [
|
||||
{
|
||||
text: '<p>男</p>',
|
||||
others: false,
|
||||
mustOthers: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '115019',
|
||||
},
|
||||
{
|
||||
text: '<p>女</p>',
|
||||
others: false,
|
||||
mustOthers: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '115020',
|
||||
},
|
||||
],
|
||||
importKey: 'single',
|
||||
importData: '',
|
||||
cOption: '',
|
||||
cOptions: [],
|
||||
nps: {
|
||||
leftText: '极不满意',
|
||||
rightText: '极满意',
|
||||
},
|
||||
star: 5,
|
||||
exclude: false,
|
||||
textRange: {
|
||||
min: {
|
||||
placeholder: '0',
|
||||
value: 0,
|
||||
},
|
||||
max: {
|
||||
placeholder: '500',
|
||||
value: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'data450',
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'text',
|
||||
placeholderDesc: '',
|
||||
title: '<p>身份证</p>',
|
||||
placeholder: '',
|
||||
valid: '',
|
||||
isRequired: true,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
starStyle: 'star',
|
||||
rangeConfig: {},
|
||||
star: 5,
|
||||
textRange: {
|
||||
min: {
|
||||
placeholder: '0',
|
||||
value: 0,
|
||||
},
|
||||
max: {
|
||||
placeholder: '500',
|
||||
value: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'data405',
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'text',
|
||||
placeholderDesc: '',
|
||||
title: '<p>地址</p>',
|
||||
placeholder: '',
|
||||
valid: '',
|
||||
isRequired: true,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
starStyle: 'star',
|
||||
rangeConfig: {},
|
||||
star: 5,
|
||||
textRange: {
|
||||
min: {
|
||||
placeholder: '0',
|
||||
value: 0,
|
||||
},
|
||||
max: {
|
||||
placeholder: '500',
|
||||
value: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'data770',
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'text',
|
||||
placeholderDesc: '',
|
||||
title: '<p>邮箱</p>',
|
||||
placeholder: '',
|
||||
valid: '',
|
||||
isRequired: true,
|
||||
randomSort: false,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
starStyle: 'star',
|
||||
rangeConfig: {},
|
||||
star: 5,
|
||||
textRange: {
|
||||
min: {
|
||||
placeholder: '0',
|
||||
value: 0,
|
||||
},
|
||||
max: {
|
||||
placeholder: '500',
|
||||
value: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
pageId: '65f29f3192862d6a9067ad1c',
|
||||
} as ResponseSchema;
|
@ -0,0 +1,141 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ResponseSchemaService } from '../services/responseScheme.service';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { mockResponseSchema } from './mockResponseSchema';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
describe('ResponseSchemaService', () => {
|
||||
let service: ResponseSchemaService;
|
||||
let responseSchemaRepository: MongoRepository<ResponseSchema>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ResponseSchemaService,
|
||||
{
|
||||
provide: getRepositoryToken(ResponseSchema),
|
||||
useValue: {
|
||||
findOne: jest.fn().mockResolvedValue(mockResponseSchema),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ResponseSchemaService>(ResponseSchemaService);
|
||||
responseSchemaRepository = module.get<MongoRepository<ResponseSchema>>(
|
||||
getRepositoryToken(ResponseSchema),
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('publishResponseSchema', () => {
|
||||
it('should update existing response schema', async () => {
|
||||
jest
|
||||
.spyOn(responseSchemaRepository, 'save')
|
||||
.mockResolvedValueOnce(undefined);
|
||||
|
||||
const params = {
|
||||
title: 'testTitle',
|
||||
surveyPath: mockResponseSchema.surveyPath,
|
||||
code: {},
|
||||
pageId: mockResponseSchema.pageId,
|
||||
};
|
||||
|
||||
await service.publishResponseSchema(params);
|
||||
|
||||
expect(responseSchemaRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
surveyPath: params.surveyPath,
|
||||
},
|
||||
});
|
||||
expect(responseSchemaRepository.create).toHaveBeenCalledTimes(0);
|
||||
expect(responseSchemaRepository.save).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should create new response schema if not exists', async () => {
|
||||
jest
|
||||
.spyOn(responseSchemaRepository, 'findOne')
|
||||
.mockResolvedValueOnce(null);
|
||||
const params = {
|
||||
title: 'testTitle',
|
||||
surveyPath: mockResponseSchema.surveyPath,
|
||||
code: {},
|
||||
pageId: mockResponseSchema.pageId,
|
||||
};
|
||||
await service.publishResponseSchema(params);
|
||||
|
||||
expect(responseSchemaRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
surveyPath: params.surveyPath,
|
||||
},
|
||||
});
|
||||
expect(responseSchemaRepository.create).toHaveBeenCalledTimes(1);
|
||||
expect(responseSchemaRepository.save).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getResponseSchemaByPath', () => {
|
||||
it('should return response schema by survey path', async () => {
|
||||
jest
|
||||
.spyOn(responseSchemaRepository, 'findOne')
|
||||
.mockResolvedValueOnce(mockResponseSchema);
|
||||
|
||||
const result = await service.getResponseSchemaByPath(
|
||||
mockResponseSchema.surveyPath,
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponseSchema);
|
||||
expect(responseSchemaRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
surveyPath: mockResponseSchema.surveyPath,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getResponseSchemaByPageId', () => {
|
||||
it('should return response schema by page ID', async () => {
|
||||
jest
|
||||
.spyOn(responseSchemaRepository, 'findOne')
|
||||
.mockResolvedValueOnce(mockResponseSchema);
|
||||
|
||||
const result = await service.getResponseSchemaByPageId(
|
||||
mockResponseSchema.pageId,
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponseSchema);
|
||||
expect(responseSchemaRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { pageId: mockResponseSchema.pageId },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteResponseSchema', () => {
|
||||
it('should delete response schema by survey path', async () => {
|
||||
jest
|
||||
.spyOn(responseSchemaRepository, 'findOne')
|
||||
.mockResolvedValueOnce(cloneDeep(mockResponseSchema));
|
||||
jest
|
||||
.spyOn(responseSchemaRepository, 'save')
|
||||
.mockResolvedValueOnce(undefined);
|
||||
|
||||
await service.deleteResponseSchema({
|
||||
surveyPath: mockResponseSchema.surveyPath,
|
||||
});
|
||||
|
||||
expect(responseSchemaRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
surveyPath: mockResponseSchema.surveyPath,
|
||||
},
|
||||
});
|
||||
expect(responseSchemaRepository.save).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
@ -4,34 +4,105 @@ import { ResponseSchemaService } from '../services/responseScheme.service';
|
||||
import { CounterService } from '../services/counter.service';
|
||||
import { SurveyResponseService } from '../services/surveyResponse.service';
|
||||
import { ClientEncryptService } from '../services/clientEncrypt.service';
|
||||
import { mockResponseSchema } from './mockResponseSchema';
|
||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
||||
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { ClientEncrypt } from 'src/models/clientEncrypt.entity';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
const mockDecryptErrorBody = {
|
||||
surveyPath: 'EBzdmnSp',
|
||||
data: [
|
||||
'SkyfsbS6MDvFrrxFJQDMxsvm53G3PTURktfZikJP2fKilC8wPW5ZdfX29Fixor5ldHBBNyILsDtxhbNahEbNCDw8n1wS8IIckFuQcaJtn6MLOD+h+Iuywka3ig4ecTN87RpdcfEQe7f38rSSx0zoFU8j37eojjSF7eETBSrz5m9WaNesQo4hhC6p7wmAo1jggkdbG8PVrFqrZPbkHN5jOBrzQEqdqYu9A5wHMM7nUteqlPpkiogEDYmBIccqmPdtO54y7LoPslXgXj6jNja8oVNaYlnp7UsisT+i5cuQ7lbDukEhrfpAIFRsT2IUwVlLjWHtFQm4/4I5HyvVBirTng==',
|
||||
'IMn0E7R6cYCQPI497mz3x99CPA4cImAFEfIv8Q98Gm5bFcgKJX6KFYS7PF/VtIuI1leKwwNYexQy7+2HnF40by/huVugoPYnPd4pTpUdG6f1kh8EpzIir2+8P98Dcz2+NZ/khP2RIAM8nOr+KSC99TNGhuKaKQCItyLFDkr80s3zv+INieGc8wULIrGoWDJGN2KdU/jSq+hkV0QXypd81N5IyAoNhZLkZeM/FU6grGFPnGRtcDDc5W8YWVHO87VymlxPCTRawXRTDcvGIUqb3GuZfxvA7AULqbspmN9kzt3rktuZLNb2TFQDsJfqUuCmi+b28qP/G4OrT9/VAHhYKw==',
|
||||
],
|
||||
difTime: 806707,
|
||||
clientTime: 1710400229573,
|
||||
encryptType: 'rsa',
|
||||
sessionId: '65f2664c92862d6a9067ad18',
|
||||
sign: '8c9ca8804c9d94de6055d68a1f3c423fe50c95b4bd69f809ee2da8fcd82fd960.1710400229589',
|
||||
};
|
||||
|
||||
import * as aes from 'crypto-js/aes';
|
||||
const mockSubmitData = {
|
||||
surveyPath: 'EBzdmnSp',
|
||||
data: [
|
||||
'SkyfsbS6MDvFrrxFJQDMxsvm53G3PTURktfZikJP2fKilC8wPW5ZdfX29Fixor5ldHBBNyILsDtxhbNahEbNCDw8n1wS8IIckFuQcaJtn6MLOD+h+Iuywka3ig4ecTN87RpdcfEQe7f38rSSx0zoFU8j37eojjSF7eETBSrz5m9WaNesQo4hhC6p7wmAo1jggkdbG8PVrFqrZPbkHN5jOBrzQEqdqYu9A5wHMM7nUteqlPpkiogEDYmBIccqmPdtO54y7LoPslXgXj6jNja8oVNaYlnp7UsisT+i5cuQ7lbDukEhrfpAIFRsT2IUwVlLjWHtFQm4/4I5HyvVBirTng==',
|
||||
'IMn0E7R6cYCQPI497mz3x99CPA4cImAFEfIv8Q98Gm5bFcgKJX6KFYS7PF/VtIuI1leKwwNYexQy7+2HnF40by/huVugoPYnPd4pTpUdG6f1kh8EpzIir2+8P98Dcz2+NZ/khP2RIAM8nOr+KSC99TNGhuKaKQCItyLFDkr80s3zv+INieGc8wULIrGoWDJGN2KdU/jSq+hkV0QXypd81N5IyAoNhZLkZeM/FU6grGFPnGRtcDDc5W8YWVHO87VymlxPCTRawXRTDcvGIUqb3GuZfxvA7AULqbspmN9kzt3rktuZLNb2TFQDsJfqUuCmi+b28qP/G4OrT9/VAHhYKw==',
|
||||
],
|
||||
difTime: 806707,
|
||||
clientTime: 1710400229573,
|
||||
encryptType: 'rsa',
|
||||
sessionId: '65f29fc192862d6a9067ad28',
|
||||
sign: '8c9ca8804c9d94de6055d68a1f3c423fe50c95b4bd69f809ee2da8fcd82fd960.1710400229589',
|
||||
};
|
||||
|
||||
jest.mock('../services/responseScheme.service');
|
||||
jest.mock('../services/counter.service');
|
||||
jest.mock('../services/surveyResponse.service');
|
||||
jest.mock('../services/clientEncrypt.service');
|
||||
jest.mock('src/utils/checkSign');
|
||||
jest.mock('crypto-js/aes');
|
||||
const mockClientEncryptInfo = {
|
||||
_id: new ObjectId('65f29fc192862d6a9067ad28'),
|
||||
data: {
|
||||
publicKey:
|
||||
'-----BEGIN PUBLIC KEY-----\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA45uWd29i6dcjLP2Cp4IV\r\naGASv+tHeaqQt8t7jojtc6rO46dD0CUkPTo9aewtuDxTHFDiKWJRJMRdXIUFNqVH\r\n1SKX7rCSG/Fh9G14pnddnSFF1eagGfvXBptycp5vKQb1IYT85zqqfORI6mGnhsQ/\r\nj+POVkIb+ANAAUXo8O/kLpVk0+cbitZYFZZWzhf+ZtSRhitlD55zonJ+Nz2hWpmr\r\npeKAG0VTRX27fDUyu2YpVFbwz7SjDsbdZ/L8XjLsUaHzRaDHL6sYYH7cWIQzj2DQ\r\nzhkR+RzeQNiSct0k7kmQ8LotWv/8sER0/yglXXH0Go42myjMI7i/2T7NpJ2ywxa3\r\nCwIDAQAB\r\n-----END PUBLIC KEY-----\r\n',
|
||||
privateKey:
|
||||
'-----BEGIN RSA PRIVATE KEY-----\r\nMIIEowIBAAKCAQEA45uWd29i6dcjLP2Cp4IVaGASv+tHeaqQt8t7jojtc6rO46dD\r\n0CUkPTo9aewtuDxTHFDiKWJRJMRdXIUFNqVH1SKX7rCSG/Fh9G14pnddnSFF1eag\r\nGfvXBptycp5vKQb1IYT85zqqfORI6mGnhsQ/j+POVkIb+ANAAUXo8O/kLpVk0+cb\r\nitZYFZZWzhf+ZtSRhitlD55zonJ+Nz2hWpmrpeKAG0VTRX27fDUyu2YpVFbwz7Sj\r\nDsbdZ/L8XjLsUaHzRaDHL6sYYH7cWIQzj2DQzhkR+RzeQNiSct0k7kmQ8LotWv/8\r\nsER0/yglXXH0Go42myjMI7i/2T7NpJ2ywxa3CwIDAQABAoIBAEfqKhGUpRkje57E\r\nftq0VFVFPcdb7Jp5lP4tkd2IUBZi2rm9aMTEZ33c//iOwidbEBt7RuoygVbvoFwS\r\nP4JzmI20P3MQYSnpC70yNZPLVU3HbIxYMS/kjZ0t0mx6uL6qzxsHLO1WcPXDH3LG\r\n5irDqR2qqdBBVRr40+lTEHXIJj29J5NNWjGcCtv8EkqzrhHjF0XypVrGsFCdm0yB\r\n3We1ypU68JC4AFzheC4ckk7Cm90oMC8eIqn8iYb4w24NYzyqDOcHupBlljHzxT8x\r\n89cy490LKI0j06+OchlSHWgy2ixO4s1futTCA789f68+ZEhtv8gNsLpY3+iI35ni\r\n/M5+VHkCgYEA+2PUd9UNkQVAQp8UVThkEgfRs4T9DF0RXD1HpzYev4gj/KbZGCw5\r\nUlOC7ufiY7MfVPil2tC9vv/pjzyATHNP1liM97AIhB/bj5V5wOvPXEGVyey/MkBw\r\n4e2cf4xFfaJL2piE/FqJ1kDrbDN4vEC8fz0lvR9NhfEVFHgtUyp3zgcCgYEA58gc\r\nQ5z7M0n1/YzDVtMcuXzeKLP1mBavelhy8W6OgGwOoMixsobEo4Rx7EFWBXNGmc8K\r\n5yXzN7UEdrpCUNGoU0j51B7q7qf+I/bp0k09q7BEKT+bEYvYaDALVxvqKHUaAafI\r\nQUWCu7TWmymHCiWtkHnMTkcyN6baJCdAaK1Qjd0CgYB12UfqYVt5x69nS/IZPVVU\r\nSowZD1gdaqfPyP6FOc7SVT0hnQoa1eiNWo7/9n7f5EHk8Ke327GID6prNp6iuFAO\r\nGPcEymZDojeoqRcpxKIyCqDwx2aeZS1GDMEX3idZjTLoKCX3s234nfh/geWwwtxa\r\n/cxqS3lpOCp8rRX6bec6EwKBgGayhcN3lN3+0V3MtuiLldih+RVz10fSFWJSOmu7\r\nHqzMNBcNlZ6SlCIXlxqlQGYd05Rm5l/Qstll/VpV4PhKTRjJ5tgT8uhXywVIbAXg\r\nb4jZCvpz0lON8Q8I6p1oIvJWIHXHT7WMBQcCc2xAlDLsyuCO9vVgGmIKLfGC6sj2\r\nshCJAoGBAL6FK1se6TqKsBdGPMqZTL5qbHrhDBeTZFVThank6Yji80jKjouLYMTK\r\nTLsu5zSvOPiDsHjYASNs4s0Hluw7OY/i4UdhoAJ5Zqy+yjtWL1ZPZueHVSse41Ip\r\n+q5VeW6LUnVxdF20RQA/S5sbcut0NTB7pjZi7YlmwksywFZooSSz\r\n-----END RSA PRIVATE KEY-----\r\n',
|
||||
},
|
||||
type: 'rsa',
|
||||
curStatus: {
|
||||
status: 'new',
|
||||
date: 1710399425273.0,
|
||||
},
|
||||
statusList: [
|
||||
{
|
||||
status: 'new',
|
||||
date: 1710399425273.0,
|
||||
},
|
||||
],
|
||||
createDate: 1710399425273.0,
|
||||
updateDate: 1710399425273.0,
|
||||
};
|
||||
|
||||
describe('SurveyResponseController', () => {
|
||||
let controller: SurveyResponseController;
|
||||
let responseSchemaService: ResponseSchemaService;
|
||||
// let counterService: CounterService;
|
||||
let surveyResponseService: SurveyResponseService;
|
||||
let clientEncryptService: ClientEncryptService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [SurveyResponseController],
|
||||
providers: [
|
||||
ResponseSchemaService,
|
||||
CounterService,
|
||||
SurveyResponseService,
|
||||
ClientEncryptService,
|
||||
{
|
||||
provide: ResponseSchemaService,
|
||||
useValue: {
|
||||
getResponseSchemaByPath: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CounterService,
|
||||
useValue: {
|
||||
get: jest.fn().mockResolvedValue(null),
|
||||
set: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SurveyResponseService,
|
||||
useValue: {
|
||||
getSurveyResponseTotalByPath: jest.fn(),
|
||||
createSurveyResponse: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ClientEncryptService,
|
||||
useValue: {
|
||||
deleteEncryptInfo: jest.fn(),
|
||||
getEncryptInfoById: jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockClientEncryptInfo),
|
||||
},
|
||||
},
|
||||
PluginManagerProvider,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@ -39,68 +110,140 @@ describe('SurveyResponseController', () => {
|
||||
responseSchemaService = module.get<ResponseSchemaService>(
|
||||
ResponseSchemaService,
|
||||
);
|
||||
// counterService = module.get<CounterService>(CounterService);
|
||||
surveyResponseService = module.get<SurveyResponseService>(
|
||||
SurveyResponseService,
|
||||
);
|
||||
clientEncryptService =
|
||||
module.get<ClientEncryptService>(ClientEncryptService);
|
||||
|
||||
const pluginManager = module.get<XiaojuSurveyPluginManager>(
|
||||
XiaojuSurveyPluginManager,
|
||||
);
|
||||
pluginManager.registerPlugin(
|
||||
new ResponseSecurityPlugin('dataAesEncryptSecretKey'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
describe('createResponse', () => {
|
||||
it('should create survey response successfully with valid parameters', async () => {
|
||||
const mockReqBody = {
|
||||
surveyPath: '5q1PbCtvPM',
|
||||
data: '%7B%22data458%22%3A%22111%22%2C%22data515%22%3A%22xhfudsdg%22%7D',
|
||||
difTime: 5687,
|
||||
clientTime: 1706103961153,
|
||||
encryptType: 'aes',
|
||||
sessionId: '65b11493e8df57de0ff04c98',
|
||||
sign: 'c7ca1a8217a9ef0f4c4ed58701899603ce446353784a22c35774240f4cf4c5a4.1706103961154',
|
||||
};
|
||||
const mockResponseSchema = {
|
||||
curStatus: { status: RECORD_STATUS.PUBLISHED, date: Date.now() },
|
||||
code: {
|
||||
dataConf: {
|
||||
dataList: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const mockClientEncryptData = {
|
||||
it('should create response successfully', async () => {
|
||||
const reqBody = cloneDeep(mockSubmitData);
|
||||
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValueOnce(mockResponseSchema);
|
||||
jest
|
||||
.spyOn(surveyResponseService, 'getSurveyResponseTotalByPath')
|
||||
.mockResolvedValueOnce(0);
|
||||
jest
|
||||
.spyOn(surveyResponseService, 'createSurveyResponse')
|
||||
.mockResolvedValueOnce(undefined);
|
||||
jest
|
||||
.spyOn(clientEncryptService, 'deleteEncryptInfo')
|
||||
.mockResolvedValueOnce(undefined);
|
||||
|
||||
const result = await controller.createResponse(reqBody);
|
||||
|
||||
expect(result).toEqual({ code: 200, msg: '提交成功' });
|
||||
expect(
|
||||
responseSchemaService.getResponseSchemaByPath,
|
||||
).toHaveBeenCalledWith(reqBody.surveyPath);
|
||||
expect(
|
||||
surveyResponseService.getSurveyResponseTotalByPath,
|
||||
).toHaveBeenCalledWith(reqBody.surveyPath);
|
||||
expect(surveyResponseService.createSurveyResponse).toHaveBeenCalledWith({
|
||||
surveyPath: reqBody.surveyPath,
|
||||
data: {
|
||||
secretKey: 'testSecretKey',
|
||||
data405: '浙江省杭州市西湖区xxx',
|
||||
data450: '450111000000000000',
|
||||
data458: '15000000000',
|
||||
data515: '115019',
|
||||
data770: '123456@qq.com',
|
||||
},
|
||||
};
|
||||
clientTime: reqBody.clientTime,
|
||||
difTime: reqBody.difTime,
|
||||
surveyId: mockResponseSchema.pageId, // mock response schema 的 pageId
|
||||
optionTextAndId: {
|
||||
data515: [
|
||||
{
|
||||
hash: '115019',
|
||||
text: '<p>男</p>',
|
||||
},
|
||||
{
|
||||
hash: '115020',
|
||||
text: '<p>女</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(clientEncryptService.deleteEncryptInfo).toHaveBeenCalledWith(
|
||||
reqBody.sessionId,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw SurveyNotFoundException if survey does not exist', async () => {
|
||||
const reqBody = cloneDeep(mockSubmitData);
|
||||
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValue(mockResponseSchema as ResponseSchema);
|
||||
jest
|
||||
.spyOn(clientEncryptService, 'getEncryptInfoById')
|
||||
.mockResolvedValue(mockClientEncryptData as ClientEncrypt);
|
||||
.mockResolvedValueOnce(null);
|
||||
|
||||
jest.spyOn(aes, 'decrypt').mockImplementation((data) => data);
|
||||
|
||||
const result = await controller.createResponse(mockReqBody);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
msg: '提交成功',
|
||||
});
|
||||
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||
SurveyNotFoundException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw SurveyNotFoundException when response schema is not found', async () => {
|
||||
const mockReqBody = {
|
||||
surveyPath: '5q1PbCtvPM',
|
||||
data: '%7B%22data458%22%3A%22111%22%2C%22data515%22%3A%22xhfudsdg%22%7D',
|
||||
encryptType: 'validEncryptType',
|
||||
sessionId: 'validSessionId',
|
||||
clientTime: 123456789,
|
||||
difTime: 0,
|
||||
};
|
||||
it('should throw HttpException if no sign', async () => {
|
||||
const reqBody = cloneDeep(mockSubmitData);
|
||||
delete reqBody.sign;
|
||||
|
||||
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
|
||||
expect(
|
||||
responseSchemaService.getResponseSchemaByPath,
|
||||
).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should throw HttpException if no sign error', async () => {
|
||||
const reqBody = cloneDeep(mockDecryptErrorBody);
|
||||
reqBody.sign = 'mock sign';
|
||||
|
||||
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
|
||||
expect(
|
||||
responseSchemaService.getResponseSchemaByPath,
|
||||
).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should throw HttpException if answer time is invalid', async () => {
|
||||
const reqBody = { surveyPath: 'validSurveyPath' };
|
||||
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValue(null);
|
||||
.mockResolvedValueOnce(mockResponseSchema);
|
||||
|
||||
await expect(controller.createResponse(mockReqBody)).rejects.toThrow(
|
||||
new SurveyNotFoundException('该问卷不存在,无法提交'),
|
||||
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw HttpException if rsa decrypt error', async () => {
|
||||
const reqBody = mockDecryptErrorBody;
|
||||
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValueOnce(mockResponseSchema);
|
||||
|
||||
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,84 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SurveyResponseService } from '../services/surveyResponse.service';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||
|
||||
describe('SurveyResponseService', () => {
|
||||
let service: SurveyResponseService;
|
||||
let surveyResponseRepository: MongoRepository<SurveyResponse>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SurveyResponseService,
|
||||
{
|
||||
provide: getRepositoryToken(SurveyResponse),
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
count: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SurveyResponseService>(SurveyResponseService);
|
||||
surveyResponseRepository = module.get<MongoRepository<SurveyResponse>>(
|
||||
getRepositoryToken(SurveyResponse),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a survey response', async () => {
|
||||
const surveyData = {
|
||||
data: {},
|
||||
clientTime: new Date(),
|
||||
difTime: 0,
|
||||
surveyId: 'testId',
|
||||
surveyPath: 'testPath',
|
||||
optionTextAndId: {},
|
||||
};
|
||||
jest
|
||||
.spyOn(surveyResponseRepository, 'create')
|
||||
.mockImplementation((data) => {
|
||||
const surveyResponse = new SurveyResponse();
|
||||
for (const key in data) {
|
||||
surveyResponse[key] = data[key];
|
||||
}
|
||||
return surveyResponse;
|
||||
});
|
||||
jest
|
||||
.spyOn(surveyResponseRepository, 'save')
|
||||
.mockImplementation((surveyResponse: SurveyResponse) => {
|
||||
return Promise.resolve(surveyResponse);
|
||||
});
|
||||
|
||||
await service.createSurveyResponse(surveyData);
|
||||
|
||||
expect(surveyResponseRepository.create).toHaveBeenCalledWith({
|
||||
surveyPath: surveyData.surveyPath,
|
||||
data: surveyData.data,
|
||||
clientTime: surveyData.clientTime,
|
||||
difTime: surveyData.difTime,
|
||||
pageId: surveyData.surveyId,
|
||||
secretKeys: [],
|
||||
optionTextAndId: surveyData.optionTextAndId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the total survey response count by path', async () => {
|
||||
const surveyPath = 'testPath';
|
||||
const count = 10;
|
||||
jest.spyOn(surveyResponseRepository, 'count').mockResolvedValue(count);
|
||||
|
||||
const result = await service.getSurveyResponseTotalByPath(surveyPath);
|
||||
|
||||
expect(result).toEqual(count);
|
||||
expect(surveyResponseRepository.count).toHaveBeenCalledWith({
|
||||
where: {
|
||||
surveyPath,
|
||||
'curStatus.status': { $ne: 'removed' },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,31 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SurveyResponseUIController } from '../controllers/surveyResponseUI.controller';
|
||||
import { Response } from 'express';
|
||||
import { join } from 'path';
|
||||
|
||||
describe('SurveyResponseUIController', () => {
|
||||
let controller: SurveyResponseUIController;
|
||||
let res: Response;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [SurveyResponseUIController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<SurveyResponseUIController>(
|
||||
SurveyResponseUIController,
|
||||
);
|
||||
res = {
|
||||
sendFile: jest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as Response;
|
||||
});
|
||||
|
||||
it('should render the survey response with the correct path', () => {
|
||||
const surveyPath = 'some-survey-path';
|
||||
const expectedFilePath = join(process.cwd(), 'public', 'render.html');
|
||||
|
||||
controller.render(surveyPath, res);
|
||||
|
||||
expect(res.sendFile).toHaveBeenCalledWith(expectedFilePath);
|
||||
});
|
||||
});
|
@ -144,7 +144,6 @@ export class SurveyResponseController {
|
||||
return pre;
|
||||
}, {});
|
||||
|
||||
const secretKeys = [];
|
||||
// 对用户提交的数据进行遍历处理
|
||||
for (const field in decryptedData) {
|
||||
const value = decryptedData[field];
|
||||
@ -180,7 +179,6 @@ export class SurveyResponseController {
|
||||
data: decryptedData,
|
||||
clientTime,
|
||||
difTime,
|
||||
secretKeys,
|
||||
surveyId: responseSchema.pageId,
|
||||
optionTextAndId,
|
||||
});
|
||||
|
@ -11,7 +11,6 @@ export class SurveyResponseService {
|
||||
|
||||
async createSurveyResponse({
|
||||
data,
|
||||
secretKeys,
|
||||
clientTime,
|
||||
difTime,
|
||||
surveyId,
|
||||
@ -21,7 +20,7 @@ export class SurveyResponseService {
|
||||
const newSubmitData = this.surveyResponseRepository.create({
|
||||
surveyPath,
|
||||
data,
|
||||
secretKeys,
|
||||
secretKeys: [],
|
||||
clientTime,
|
||||
difTime,
|
||||
pageId: surveyId,
|
||||
|
5
server/src/utils/hash256.ts
Normal file
5
server/src/utils/hash256.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
export function hash256(text) {
|
||||
return createHash('sha256').update(text).digest('hex');
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
const surveyPathAlphabet =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
export function genSurveyPath({ size, prefix } = { size: 8, prefix: '' }) {
|
||||
size = Number(size) ? Number(size) : 8;
|
||||
const id = customAlphabet(`${prefix || ''}${surveyPathAlphabet}`, size);
|
||||
return id();
|
||||
}
|
73
server/src/utils/surveyUtil.ts
Normal file
73
server/src/utils/surveyUtil.ts
Normal file
@ -0,0 +1,73 @@
|
||||
export type FilterItem = {
|
||||
comparator?: string;
|
||||
condition: Array<FilterCondition>;
|
||||
};
|
||||
|
||||
export type FilterCondition = {
|
||||
field: string;
|
||||
comparator?: string;
|
||||
value: string & Array<FilterItem>;
|
||||
};
|
||||
|
||||
export type OrderItem = {
|
||||
field: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
export function getFilter(filterList: Array<FilterItem>) {
|
||||
const allowFilterField = [
|
||||
'title',
|
||||
'remark',
|
||||
'surveyType',
|
||||
'curStatus.status',
|
||||
];
|
||||
return filterList.reduce(
|
||||
(preItem, curItem) => {
|
||||
const condition = curItem.condition
|
||||
.filter((item) => allowFilterField.includes(item.field))
|
||||
.reduce((pre, cur) => {
|
||||
switch (cur.comparator) {
|
||||
case '$ne':
|
||||
pre[cur.field] = {
|
||||
$ne: cur.value,
|
||||
};
|
||||
break;
|
||||
case '$regex':
|
||||
pre[cur.field] = {
|
||||
$regex: cur.value,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
pre[cur.field] = cur.value;
|
||||
break;
|
||||
}
|
||||
return pre;
|
||||
}, {});
|
||||
switch (curItem.comparator) {
|
||||
case '$or':
|
||||
if (!Array.isArray(preItem.$or)) {
|
||||
preItem.$or = [];
|
||||
}
|
||||
preItem.$or.push(condition);
|
||||
break;
|
||||
default:
|
||||
Object.assign(preItem, condition);
|
||||
break;
|
||||
}
|
||||
return preItem;
|
||||
},
|
||||
{} as { $or?: Array<Record<string, string>> } & Record<string, string>,
|
||||
);
|
||||
}
|
||||
|
||||
export function getOrder(order: Array<OrderItem>) {
|
||||
const allowOrderFields = ['createDate', 'updateDate', 'curStatus.date'];
|
||||
|
||||
const orderList = order.filter((orderItem) =>
|
||||
allowOrderFields.includes(orderItem.field),
|
||||
);
|
||||
return orderList.reduce((pre, cur) => {
|
||||
pre[cur.field] = cur.value === 1 ? 1 : -1;
|
||||
return pre;
|
||||
}, {});
|
||||
}
|
Loading…
Reference in New Issue
Block a user