From 8d29865ff0982b6e21e5e8e3478cdac03c8e1432 Mon Sep 17 00:00:00 2001 From: luch <32321690+luch1994@users.noreply.github.com> Date: Thu, 14 Mar 2024 21:50:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E5=8D=95=E6=B5=8B=20?= =?UTF-8?q?(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/.env | 2 +- server/package.json | 2 +- server/public/index.html | 4 +- server/public/management.html | 4 +- server/public/render.html | 4 +- server/src/app.controller.spec.ts | 18 + server/src/app.module.ts | 5 +- server/src/guards/authtication.spec.ts | 126 ++++ server/src/interfaces/survey.ts | 25 +- server/src/logger/index.ts | 22 +- server/src/logger/logger.provider.ts | 4 +- .../src/middlewares/logRequest.middleware.ts | 2 +- server/src/models/__test/base.entity.spec.ts | 30 + .../__test/surveyResponse.entity.spec.ts | 42 ++ server/src/models/base.entity.ts | 43 ++ server/src/models/captcha.entity.ts | 48 +- server/src/models/clientEncrypt.entity.ts | 48 +- server/src/models/counter.entity.ts | 51 +- server/src/models/responseSchema.entity.ts | 51 +- server/src/models/surveyConf.entity.ts | 52 +- server/src/models/surveyHistory.entity.ts | 52 +- server/src/models/surveyMeta.entity.ts | 51 +- server/src/models/surveyResponse.entity.ts | 56 +- server/src/models/user.entity.ts | 52 +- server/src/models/word.entity.ts | 52 +- .../auth.controller.spec.ts | 22 +- .../{services => __test}/auth.service.spec.ts | 2 +- .../captcha.service.spec.ts | 2 +- .../modules/auth/__test/user.service.spec.ts | 142 ++++ .../src/modules/auth/services/user.service.ts | 10 +- .../__test/dataStatistic.controller.spec.ts | 158 +++++ .../__test/dataStatistic.service.spec.ts | 313 +++++++++ .../survey/__test/mockResponseSchema.ts | 637 ++++++++++++++++++ .../survey/__test/survey.controller.spec.ts | 52 +- .../survey/__test/surveyConf.service.spec.ts | 152 +++++ .../__test/surveyHistory.controller.spec.ts | 74 ++ .../__test/surveyHistory.service.spec.ts | 121 ++++ .../__test/surveyMeta.controller.spec.ts | 200 ++++++ .../survey/__test/surveyMeta.service.spec.ts | 264 ++++++++ .../survey/__test/surveyUI.controller.spec.ts | 32 + .../controllers/surveyMeta.controller.ts | 78 +-- .../survey/controllers/surveyUI.controller.ts | 6 +- .../survey/services/dataStatistic.service.ts | 44 +- .../survey/services/surveyConf.service.ts | 43 +- .../survey/services/surveyMeta.service.ts | 6 +- server/src/modules/survey/utils/index.ts | 67 ++ .../__test/clientEncrypt.service.spec.ts | 119 ++++ .../__test/counter.service.spec.ts | 103 +++ .../__test/mockResponseSchema.ts | 239 +++++++ .../__test/responseScheme.service.spec.ts | 141 ++++ .../__test/surveyResponse.controller.spec.ts | 259 +++++-- .../__test/surveyResponse.service.spec.ts | 84 +++ .../surveyResponseUI.controller.spec.ts | 31 + .../controllers/surveyResponse.controller.ts | 2 - .../services/surveyResponse.service.ts | 3 +- server/src/utils/hash256.ts | 5 + server/src/utils/index.ts | 10 - server/src/utils/surveyUtil.ts | 73 ++ 58 files changed, 3580 insertions(+), 760 deletions(-) create mode 100644 server/src/app.controller.spec.ts create mode 100644 server/src/guards/authtication.spec.ts create mode 100644 server/src/models/__test/base.entity.spec.ts create mode 100644 server/src/models/__test/surveyResponse.entity.spec.ts create mode 100644 server/src/models/base.entity.ts rename server/src/modules/auth/{controllers => __test}/auth.controller.spec.ts (86%) rename server/src/modules/auth/{services => __test}/auth.service.spec.ts (93%) rename server/src/modules/auth/{services => __test}/captcha.service.spec.ts (98%) create mode 100644 server/src/modules/auth/__test/user.service.spec.ts create mode 100644 server/src/modules/survey/__test/dataStatistic.controller.spec.ts create mode 100644 server/src/modules/survey/__test/dataStatistic.service.spec.ts create mode 100644 server/src/modules/survey/__test/mockResponseSchema.ts create mode 100644 server/src/modules/survey/__test/surveyConf.service.spec.ts create mode 100644 server/src/modules/survey/__test/surveyHistory.controller.spec.ts create mode 100644 server/src/modules/survey/__test/surveyHistory.service.spec.ts create mode 100644 server/src/modules/survey/__test/surveyMeta.controller.spec.ts create mode 100644 server/src/modules/survey/__test/surveyMeta.service.spec.ts create mode 100644 server/src/modules/survey/__test/surveyUI.controller.spec.ts create mode 100644 server/src/modules/survey/utils/index.ts create mode 100644 server/src/modules/surveyResponse/__test/clientEncrypt.service.spec.ts create mode 100644 server/src/modules/surveyResponse/__test/counter.service.spec.ts create mode 100644 server/src/modules/surveyResponse/__test/mockResponseSchema.ts create mode 100644 server/src/modules/surveyResponse/__test/responseScheme.service.spec.ts create mode 100644 server/src/modules/surveyResponse/__test/surveyResponse.service.spec.ts create mode 100644 server/src/modules/surveyResponse/__test/surveyResponseUI.controller.spec.ts create mode 100644 server/src/utils/hash256.ts delete mode 100644 server/src/utils/index.ts create mode 100644 server/src/utils/surveyUtil.ts diff --git a/server/.env b/server/.env index 4c495386..c039a348 100644 --- a/server/.env +++ b/server/.env @@ -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 diff --git a/server/package.json b/server/package.json index 0cef4880..d0a7fa02 100644 --- a/server/package.json +++ b/server/package.json @@ -87,4 +87,4 @@ "^src/(.*)$": "/$1" } } -} +} \ No newline at end of file diff --git a/server/public/index.html b/server/public/index.html index 3378ab44..d32d791a 100644 --- a/server/public/index.html +++ b/server/public/index.html @@ -5,11 +5,11 @@ 问卷管理端 - +
- +

暂无数据

diff --git a/server/public/management.html b/server/public/management.html index 3378ab44..d32d791a 100644 --- a/server/public/management.html +++ b/server/public/management.html @@ -5,11 +5,11 @@ 问卷管理端 - +
- +

暂无数据

diff --git a/server/public/render.html b/server/public/render.html index 3205ee84..df4bebf2 100644 --- a/server/public/render.html +++ b/server/public/render.html @@ -5,11 +5,11 @@ 问卷投放端 - +
- +

暂无数据

diff --git a/server/src/app.controller.spec.ts b/server/src/app.controller.spec.ts new file mode 100644 index 00000000..aec925a1 --- /dev/null +++ b/server/src/app.controller.spec.ts @@ -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); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/server/src/app.module.ts b/server/src/app.module.ts index edcb3994..eabda8b9 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -46,7 +46,7 @@ import { Logger } from './logger'; const authSource = (await configService.get( 'XIAOJU_SURVEY_MONGO_AUTH_SOURCE', - )) || ''; + )) || 'admin'; const database = await configService.get( '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('XIAOJU_SURVEY_LOGGER_FILENAME'), }); } diff --git a/server/src/guards/authtication.spec.ts b/server/src/guards/authtication.spec.ts new file mode 100644 index 00000000..b5a0e981 --- /dev/null +++ b/server/src/guards/authtication.spec.ts @@ -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); + userService = module.get(UserService); + configService = module.get(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); + }); +}); diff --git a/server/src/interfaces/survey.ts b/server/src/interfaces/survey.ts index a281b8ce..30403597 100644 --- a/server/src/interfaces/survey.ts +++ b/server/src/interfaces/survey.ts @@ -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; } diff --git a/server/src/logger/index.ts b/server/src/logger/index.ts index c54ce1b4..d9eda72b 100644 --- a/server/src/logger/index.ts +++ b/server/src/logger/index.ts @@ -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(); diff --git a/server/src/logger/logger.provider.ts b/server/src/logger/logger.provider.ts index 513b7690..2a298dd4 100644 --- a/server/src/logger/logger.provider.ts +++ b/server/src/logger/logger.provider.ts @@ -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, }; diff --git a/server/src/middlewares/logRequest.middleware.ts b/server/src/middlewares/logRequest.middleware.ts index 7c5d34bd..dac00d01 100644 --- a/server/src/middlewares/logRequest.middleware.ts +++ b/server/src/middlewares/logRequest.middleware.ts @@ -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( diff --git a/server/src/models/__test/base.entity.spec.ts b/server/src/models/__test/base.entity.spec.ts new file mode 100644 index 00000000..cd7a78f9 --- /dev/null +++ b/server/src/models/__test/base.entity.spec.ts @@ -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 + }); +}); diff --git a/server/src/models/__test/surveyResponse.entity.spec.ts b/server/src/models/__test/surveyResponse.entity.spec.ts new file mode 100644 index 00000000..f818fee4 --- /dev/null +++ b/server/src/models/__test/surveyResponse.entity.spec.ts @@ -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); + }); +}); diff --git a/server/src/models/base.entity.ts b/server/src/models/base.entity.ts new file mode 100644 index 00000000..6f30e043 --- /dev/null +++ b/server/src/models/base.entity.ts @@ -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(); + } +} diff --git a/server/src/models/captcha.entity.ts b/server/src/models/captcha.entity.ts index f3da9b6d..55e1c45d 100644 --- a/server/src/models/captcha.entity.ts +++ b/server/src/models/captcha.entity.ts @@ -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(); - } } diff --git a/server/src/models/clientEncrypt.entity.ts b/server/src/models/clientEncrypt.entity.ts index d67ed934..eaaebdbc 100644 --- a/server/src/models/clientEncrypt.entity.ts +++ b/server/src/models/clientEncrypt.entity.ts @@ -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(); - } } diff --git a/server/src/models/counter.entity.ts b/server/src/models/counter.entity.ts index cf9f3959..93c1d677 100644 --- a/server/src/models/counter.entity.ts +++ b/server/src/models/counter.entity.ts @@ -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; - - @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(); - } } diff --git a/server/src/models/responseSchema.entity.ts b/server/src/models/responseSchema.entity.ts index 5c076723..60ee6fe8 100644 --- a/server/src/models/responseSchema.entity.ts +++ b/server/src/models/responseSchema.entity.ts @@ -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(); - } } diff --git a/server/src/models/surveyConf.entity.ts b/server/src/models/surveyConf.entity.ts index 499b51ed..4848666d 100644 --- a/server/src/models/surveyConf.entity.ts +++ b/server/src/models/surveyConf.entity.ts @@ -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(); - } } diff --git a/server/src/models/surveyHistory.entity.ts b/server/src/models/surveyHistory.entity.ts index bd721660..5d7650e4 100644 --- a/server/src/models/surveyHistory.entity.ts +++ b/server/src/models/surveyHistory.entity.ts @@ -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(); - } } diff --git a/server/src/models/surveyMeta.entity.ts b/server/src/models/surveyMeta.entity.ts index 7cd78840..5ea066cd 100644 --- a/server/src/models/surveyMeta.entity.ts +++ b/server/src/models/surveyMeta.entity.ts @@ -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(); - } } diff --git a/server/src/models/surveyResponse.entity.ts b/server/src/models/surveyResponse.entity.ts index 798050a0..adce23d3 100644 --- a/server/src/models/surveyResponse.entity.ts +++ b/server/src/models/surveyResponse.entity.ts @@ -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; - @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); } } diff --git a/server/src/models/user.entity.ts b/server/src/models/user.entity.ts index b0ffbd32..9b3ac7df 100644 --- a/server/src/models/user.entity.ts +++ b/server/src/models/user.entity.ts @@ -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(); - } } diff --git a/server/src/models/word.entity.ts b/server/src/models/word.entity.ts index 8610f1e9..6e14f00a 100644 --- a/server/src/models/word.entity.ts +++ b/server/src/models/word.entity.ts @@ -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(); - } } diff --git a/server/src/modules/auth/controllers/auth.controller.spec.ts b/server/src/modules/auth/__test/auth.controller.spec.ts similarity index 86% rename from server/src/modules/auth/controllers/auth.controller.spec.ts rename to server/src/modules/auth/__test/auth.controller.spec.ts index 7b32ad6b..b3806c08 100644 --- a/server/src/modules/auth/controllers/auth.controller.spec.ts +++ b/server/src/modules/auth/__test/auth.controller.spec.ts @@ -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'); + }); + }); }); diff --git a/server/src/modules/auth/services/auth.service.spec.ts b/server/src/modules/auth/__test/auth.service.spec.ts similarity index 93% rename from server/src/modules/auth/services/auth.service.spec.ts rename to server/src/modules/auth/__test/auth.service.spec.ts index efc2c596..6bcc9d36 100644 --- a/server/src/modules/auth/services/auth.service.spec.ts +++ b/server/src/modules/auth/__test/auth.service.spec.ts @@ -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'); diff --git a/server/src/modules/auth/services/captcha.service.spec.ts b/server/src/modules/auth/__test/captcha.service.spec.ts similarity index 98% rename from server/src/modules/auth/services/captcha.service.spec.ts rename to server/src/modules/auth/__test/captcha.service.spec.ts index 362a8f70..62be802f 100644 --- a/server/src/modules/auth/services/captcha.service.spec.ts +++ b/server/src/modules/auth/__test/captcha.service.spec.ts @@ -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'; diff --git a/server/src/modules/auth/__test/user.service.spec.ts b/server/src/modules/auth/__test/user.service.spec.ts new file mode 100644 index 00000000..a7e39823 --- /dev/null +++ b/server/src/modules/auth/__test/user.service.spec.ts @@ -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; + + 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); + userRepository = module.get>( + 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); + }); +}); diff --git a/server/src/modules/auth/services/user.service.ts b/server/src/modules/auth/services/user.service.ts index 120722c9..74554076 100644 --- a/server/src/modules/auth/services/user.service.ts +++ b/server/src/modules/auth/services/user.service.ts @@ -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, ) {} - 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 }, }); diff --git a/server/src/modules/survey/__test/dataStatistic.controller.spec.ts b/server/src/modules/survey/__test/dataStatistic.controller.spec.ts new file mode 100644 index 00000000..e7685f50 --- /dev/null +++ b/server/src/modules/survey/__test/dataStatistic.controller.spec.ts @@ -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); + dataStatisticService = + module.get(DataStatisticService); + surveyMetaService = module.get(SurveyMetaService); + const pluginManager = module.get( + 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, + }); + }); + }); +}); diff --git a/server/src/modules/survey/__test/dataStatistic.service.spec.ts b/server/src/modules/survey/__test/dataStatistic.service.spec.ts new file mode 100644 index 00000000..830c7b59 --- /dev/null +++ b/server/src/modules/survey/__test/dataStatistic.service.spec.ts @@ -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; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DataStatisticService, + { + provide: getRepositoryToken(SurveyResponse), + useClass: MongoRepository, + }, + PluginManagerProvider, + ], + }).compile(); + + service = module.get(DataStatisticService); + surveyResponseRepository = module.get>( + getRepositoryToken(SurveyResponse), + ); + const manager = module.get( + 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: '

选项1

', + }, + { + hash: '115020', + text: '

选项2

', + }, + { + hash: '119074', + text: '

选项

', + }, + ], + data997: [ + { + hash: '211974', + text: '

选项1

', + }, + { + hash: '842501', + text: '

选项2

', + }, + { + hash: '650873', + text: '

选项

', + }, + ], + data517: [ + { + hash: '917392', + text: '对', + }, + { + hash: '156728', + text: '错', + }, + ], + data413: [ + { + hash: '502734', + text: '选项1', + }, + { + hash: '278946', + text: '选项2', + }, + ], + data863: [ + { + hash: '109239', + text: '

选项1

', + }, + { + hash: '899262', + text: '

选项2

', + }, + ], + }, + 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; + + 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 = [ + { + _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: '

', + }, + { + hash: '115020', + text: '

', + }, + ], + 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; + + 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), + }), + ]), + ); + }); + }); +}); diff --git a/server/src/modules/survey/__test/mockResponseSchema.ts b/server/src/modules/survey/__test/mockResponseSchema.ts new file mode 100644 index 00000000..c0e44602 --- /dev/null +++ b/server/src/modules/survey/__test/mockResponseSchema.ts @@ -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: + '

欢迎填写问卷

为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,期待您的参与!

', + 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: '

您的手机号

', + 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: '

您的性别

', + placeholder: '', + randomSort: false, + checked: false, + minNum: '', + maxNum: '', + options: [ + { + text: '

', + others: false, + mustOthers: false, + othersKey: '', + placeholderDesc: '', + hash: '115019', + }, + { + text: '

', + 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: '

身份证

', + 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: '

地址

', + 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: '

邮箱

', + 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: + '

欢迎填写问卷

为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,期待您的参与!

', + 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: '

提交成功

', + 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: '

选项1

', + others: true, + mustOthers: false, + othersKey: 'data515_115019', + placeholderDesc: '', + hash: '115019', + }, + { + text: '

选项2

', + others: false, + mustOthers: false, + othersKey: '', + placeholderDesc: '', + hash: '115020', + }, + { + text: '

选项

', + 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: '

选项1

', + others: true, + othersKey: 'data997_211974', + placeholderDesc: '', + hash: '211974', + }, + { + text: '

选项2

', + others: false, + othersKey: '', + placeholderDesc: '', + hash: '842501', + }, + { + text: '

选项

', + 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: '

选项1

', + others: false, + othersKey: '', + placeholderDesc: '', + hash: '109239', + }, + { + text: '

选项2

', + others: false, + othersKey: '', + placeholderDesc: '', + hash: '899262', + }, + ], + star: 5, + innerType: 'radio', + }, + ], + }, + }, + pageId: '65afc62904d5db18534c0f78', + createDate: 1710340841289, + updateDate: 1710340841289.0, +} as ResponseSchema; diff --git a/server/src/modules/survey/__test/survey.controller.spec.ts b/server/src/modules/survey/__test/survey.controller.spec.ts index 6d98c340..b1202c2e 100644 --- a/server/src/modules/survey/__test/survey.controller.spec.ts +++ b/server/src/modules/survey/__test/survey.controller.spec.ts @@ -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, + ), + ); + }); }); }); diff --git a/server/src/modules/survey/__test/surveyConf.service.spec.ts b/server/src/modules/survey/__test/surveyConf.service.spec.ts new file mode 100644 index 00000000..824af2ae --- /dev/null +++ b/server/src/modules/survey/__test/surveyConf.service.spec.ts @@ -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; + + 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); + surveyConfRepository = module.get>( + 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', + }); + }); +}); diff --git a/server/src/modules/survey/__test/surveyHistory.controller.spec.ts b/server/src/modules/survey/__test/surveyHistory.controller.spec.ts new file mode 100644 index 00000000..1ae46fec --- /dev/null +++ b/server/src/modules/survey/__test/surveyHistory.controller.spec.ts @@ -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); + surveyHistoryService = + module.get(SurveyHistoryService); + surveyMetaService = module.get(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); + }); +}); diff --git a/server/src/modules/survey/__test/surveyHistory.service.spec.ts b/server/src/modules/survey/__test/surveyHistory.service.spec.ts new file mode 100644 index 00000000..5c856a1b --- /dev/null +++ b/server/src/modules/survey/__test/surveyHistory.service.spec.ts @@ -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; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SurveyHistoryService, + { + provide: getRepositoryToken(SurveyHistory), + useClass: MongoRepository, + }, + ], + }).compile(); + + service = module.get(SurveyHistoryService); + repository = module.get>( + 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, + }); + }); + }); +}); diff --git a/server/src/modules/survey/__test/surveyMeta.controller.spec.ts b/server/src/modules/survey/__test/surveyMeta.controller.spec.ts new file mode 100644 index 00000000..1835d88a --- /dev/null +++ b/server/src/modules/survey/__test/surveyMeta.controller.spec.ts @@ -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); + surveyMetaService = module.get(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; + }); + + 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); + } + }); +}); diff --git a/server/src/modules/survey/__test/surveyMeta.service.spec.ts b/server/src/modules/survey/__test/surveyMeta.service.spec.ts new file mode 100644 index 00000000..7cdc674f --- /dev/null +++ b/server/src/modules/survey/__test/surveyMeta.service.spec.ts @@ -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; + 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); + surveyRepository = module.get>( + getRepositoryToken(SurveyMeta), + ); + pluginManager = module.get( + 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; + 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); + }); + }); +}); diff --git a/server/src/modules/survey/__test/surveyUI.controller.spec.ts b/server/src/modules/survey/__test/surveyUI.controller.spec.ts new file mode 100644 index 00000000..9898da7e --- /dev/null +++ b/server/src/modules/survey/__test/surveyUI.controller.spec.ts @@ -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); + 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); + }); +}); diff --git a/server/src/modules/survey/controllers/surveyMeta.controller.ts b/server/src/modules/survey/controllers/surveyMeta.controller.ts index 7787363e..c1a7b8e5 100644 --- a/server/src/modules/survey/controllers/surveyMeta.controller.ts +++ b/server/src/modules/survey/controllers/surveyMeta.controller.ts @@ -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; -}; - -type FilterCondition = { - field: string; - comparator?: string; - value: string & Array; -}; - -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) { - 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, - ); - } - - private getOrder(order: Array) { - 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; - }, {}); - } } diff --git a/server/src/modules/survey/controllers/surveyUI.controller.ts b/server/src/modules/survey/controllers/surveyUI.controller.ts index 44a148ea..b1ec37ac 100644 --- a/server/src/modules/survey/controllers/surveyUI.controller.ts +++ b/server/src/modules/survey/controllers/surveyUI.controller.ts @@ -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')); } } diff --git a/server/src/modules/survey/services/dataStatistic.service.ts b/server/src/modules/survey/services/dataStatistic.service.ts index a8880828..16926904 100644 --- a/server/src/modules/survey/services/dataStatistic.service.ts +++ b/server/src/modules/survey/services/dataStatistic.service.ts @@ -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, ) {} - 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, diff --git a/server/src/modules/survey/services/surveyConf.service.ts b/server/src/modules/survey/services/surveyConf.service.ts index b7c4e8c7..75baeccb 100644 --- a/server/src/modules/survey/services/surveyConf.service.ts +++ b/server/src/modules/survey/services/surveyConf.service.ts @@ -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, ) {} - 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({ diff --git a/server/src/modules/survey/services/surveyMeta.service.ts b/server/src/modules/survey/services/surveyMeta.service.ts index 50877225..2b55a93e 100644 --- a/server/src/modules/survey/services/surveyMeta.service.ts +++ b/server/src/modules/survey/services/surveyMeta.service.ts @@ -18,8 +18,8 @@ export class SurveyMetaService { private readonly pluginManager: XiaojuSurveyPluginManager, ) {} - private async getNewSurveyPath(): Promise { - let surveyPath = this.pluginManager.triggerHook('genSurveyPath'); + async getNewSurveyPath(): Promise { + 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; } diff --git a/server/src/modules/survey/utils/index.ts b/server/src/modules/survey/utils/index.ts new file mode 100644 index 00000000..3d0ffc12 --- /dev/null +++ b/server/src/modules/survey/utils/index.ts @@ -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; +} diff --git a/server/src/modules/surveyResponse/__test/clientEncrypt.service.spec.ts b/server/src/modules/surveyResponse/__test/clientEncrypt.service.spec.ts new file mode 100644 index 00000000..9d2418c6 --- /dev/null +++ b/server/src/modules/surveyResponse/__test/clientEncrypt.service.spec.ts @@ -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; + + 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); + repository = module.get>( + 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); + }); + }); +}); diff --git a/server/src/modules/surveyResponse/__test/counter.service.spec.ts b/server/src/modules/surveyResponse/__test/counter.service.spec.ts new file mode 100644 index 00000000..41cde4eb --- /dev/null +++ b/server/src/modules/surveyResponse/__test/counter.service.spec.ts @@ -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; + + 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); + counterRepository = module.get>( + 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; + 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', + }, + }); + }); +}); diff --git a/server/src/modules/surveyResponse/__test/mockResponseSchema.ts b/server/src/modules/surveyResponse/__test/mockResponseSchema.ts new file mode 100644 index 00000000..bef78ad8 --- /dev/null +++ b/server/src/modules/surveyResponse/__test/mockResponseSchema.ts @@ -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: + '

欢迎填写问卷

为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,期待您的参与!

', + 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: '

您的手机号

', + 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: '

您的性别

', + placeholder: '', + randomSort: false, + checked: false, + minNum: '', + maxNum: '', + options: [ + { + text: '

', + others: false, + mustOthers: false, + othersKey: '', + placeholderDesc: '', + hash: '115019', + }, + { + text: '

', + 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: '

身份证

', + 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: '

地址

', + 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: '

邮箱

', + 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; diff --git a/server/src/modules/surveyResponse/__test/responseScheme.service.spec.ts b/server/src/modules/surveyResponse/__test/responseScheme.service.spec.ts new file mode 100644 index 00000000..06503e4b --- /dev/null +++ b/server/src/modules/surveyResponse/__test/responseScheme.service.spec.ts @@ -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; + + 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); + responseSchemaRepository = module.get>( + 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); + }); + }); +}); diff --git a/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts b/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts index b4eca972..91d76315 100644 --- a/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts +++ b/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts @@ -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, ); + // counterService = module.get(CounterService); + surveyResponseService = module.get( + SurveyResponseService, + ); clientEncryptService = module.get(ClientEncryptService); + + const pluginManager = module.get( + 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 = { - data: { - secretKey: 'testSecretKey', - }, - }; + it('should create response successfully', async () => { + const reqBody = cloneDeep(mockSubmitData); jest .spyOn(responseSchemaService, 'getResponseSchemaByPath') - .mockResolvedValue(mockResponseSchema as ResponseSchema); + .mockResolvedValueOnce(mockResponseSchema); jest - .spyOn(clientEncryptService, 'getEncryptInfoById') - .mockResolvedValue(mockClientEncryptData as ClientEncrypt); + .spyOn(surveyResponseService, 'getSurveyResponseTotalByPath') + .mockResolvedValueOnce(0); + jest + .spyOn(surveyResponseService, 'createSurveyResponse') + .mockResolvedValueOnce(undefined); + jest + .spyOn(clientEncryptService, 'deleteEncryptInfo') + .mockResolvedValueOnce(undefined); - jest.spyOn(aes, 'decrypt').mockImplementation((data) => data); + const result = await controller.createResponse(reqBody); - const result = await controller.createResponse(mockReqBody); - - expect(result).toEqual({ - code: 200, - msg: '提交成功', + 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: { + 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: '

', + }, + { + hash: '115020', + text: '

', + }, + ], + }, }); + expect(clientEncryptService.deleteEncryptInfo).toHaveBeenCalledWith( + reqBody.sessionId, + ); }); - 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 SurveyNotFoundException if survey does not exist', async () => { + const reqBody = cloneDeep(mockSubmitData); jest .spyOn(responseSchemaService, 'getResponseSchemaByPath') - .mockResolvedValue(null); + .mockResolvedValueOnce(null); - await expect(controller.createResponse(mockReqBody)).rejects.toThrow( - new SurveyNotFoundException('该问卷不存在,无法提交'), + await expect(controller.createResponse(reqBody)).rejects.toThrow( + SurveyNotFoundException, + ); + }); + + 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') + .mockResolvedValueOnce(mockResponseSchema); + + 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, ); }); }); diff --git a/server/src/modules/surveyResponse/__test/surveyResponse.service.spec.ts b/server/src/modules/surveyResponse/__test/surveyResponse.service.spec.ts new file mode 100644 index 00000000..91d9cb37 --- /dev/null +++ b/server/src/modules/surveyResponse/__test/surveyResponse.service.spec.ts @@ -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; + + 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); + surveyResponseRepository = module.get>( + 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' }, + }, + }); + }); +}); diff --git a/server/src/modules/surveyResponse/__test/surveyResponseUI.controller.spec.ts b/server/src/modules/surveyResponse/__test/surveyResponseUI.controller.spec.ts new file mode 100644 index 00000000..ae0a6e71 --- /dev/null +++ b/server/src/modules/surveyResponse/__test/surveyResponseUI.controller.spec.ts @@ -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, + ); + 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); + }); +}); diff --git a/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts b/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts index 3b9f2858..5e5b68ad 100644 --- a/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts +++ b/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts @@ -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, }); diff --git a/server/src/modules/surveyResponse/services/surveyResponse.service.ts b/server/src/modules/surveyResponse/services/surveyResponse.service.ts index ec7b68dd..341bb44d 100644 --- a/server/src/modules/surveyResponse/services/surveyResponse.service.ts +++ b/server/src/modules/surveyResponse/services/surveyResponse.service.ts @@ -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, diff --git a/server/src/utils/hash256.ts b/server/src/utils/hash256.ts new file mode 100644 index 00000000..978c0d69 --- /dev/null +++ b/server/src/utils/hash256.ts @@ -0,0 +1,5 @@ +import { createHash } from 'crypto'; + +export function hash256(text) { + return createHash('sha256').update(text).digest('hex'); +} diff --git a/server/src/utils/index.ts b/server/src/utils/index.ts deleted file mode 100644 index 1c815c8d..00000000 --- a/server/src/utils/index.ts +++ /dev/null @@ -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(); -} diff --git a/server/src/utils/surveyUtil.ts b/server/src/utils/surveyUtil.ts new file mode 100644 index 00000000..a67cf157 --- /dev/null +++ b/server/src/utils/surveyUtil.ts @@ -0,0 +1,73 @@ +export type FilterItem = { + comparator?: string; + condition: Array; +}; + +export type FilterCondition = { + field: string; + comparator?: string; + value: string & Array; +}; + +export type OrderItem = { + field: string; + value: number; +}; + +export function getFilter(filterList: Array) { + 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, + ); +} + +export function getOrder(order: Array) { + 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; + }, {}); +}