parent
91838194bb
commit
00d5e98712
2
server/.gitignore
vendored
2
server/.gitignore
vendored
@ -36,3 +36,5 @@ lerna-debug.log*
|
|||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
tmp
|
@ -51,8 +51,12 @@
|
|||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
|
"@types/ali-oss": "^6.16.11",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
|
"@types/lodash": "^4.17.0",
|
||||||
"@types/multer": "^1.4.11",
|
"@types/multer": "^1.4.11",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@types/node-forge": "^1.3.11",
|
"@types/node-forge": "^1.3.11",
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { Authtication } from './authtication';
|
|
||||||
import { UserService } from '../modules/auth/services/user.service';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { AuthenticationException } from '../exceptions/authException';
|
import { Authtication } from './authtication';
|
||||||
|
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||||
|
import { AuthenticationException } from 'src/exceptions/authException';
|
||||||
import { User } from 'src/models/user.entity';
|
import { User } from 'src/models/user.entity';
|
||||||
import * as jwt from 'jsonwebtoken';
|
|
||||||
|
|
||||||
jest.mock('jsonwebtoken');
|
jest.mock('jsonwebtoken');
|
||||||
|
|
||||||
describe('Authtication', () => {
|
describe('Authtication', () => {
|
||||||
let guard: Authtication;
|
let guard: Authtication;
|
||||||
let userService: UserService;
|
let authService: AuthService;
|
||||||
let configService: ConfigService;
|
let configService: ConfigService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -18,9 +17,9 @@ describe('Authtication', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
Authtication,
|
Authtication,
|
||||||
{
|
{
|
||||||
provide: UserService,
|
provide: AuthService,
|
||||||
useValue: {
|
useValue: {
|
||||||
getUserByUsername: jest.fn(),
|
verifyToken: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -33,7 +32,7 @@ describe('Authtication', () => {
|
|||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
guard = module.get<Authtication>(Authtication);
|
guard = module.get<Authtication>(Authtication);
|
||||||
userService = module.get<UserService>(UserService);
|
authService = module.get<AuthService>(AuthService);
|
||||||
configService = module.get<ConfigService>(ConfigService);
|
configService = module.get<ConfigService>(ConfigService);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -62,7 +61,9 @@ describe('Authtication', () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.spyOn(jwt, 'verify').mockReturnValue(new Error('token is invalid'));
|
jest
|
||||||
|
.spyOn(authService, 'verifyToken')
|
||||||
|
.mockRejectedValue(new Error('token is invalid'));
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(configService, 'get')
|
.spyOn(configService, 'get')
|
||||||
@ -73,31 +74,6 @@ describe('Authtication', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
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(
|
|
||||||
AuthenticationException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set user in request object and return true if user exists', async () => {
|
it('should set user in request object and return true if user exists', async () => {
|
||||||
const request = {
|
const request = {
|
||||||
headers: {
|
headers: {
|
||||||
@ -114,9 +90,7 @@ describe('Authtication', () => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(configService, 'get')
|
.spyOn(configService, 'get')
|
||||||
.mockReturnValue('XIAOJU_SURVEY_JWT_SECRET');
|
.mockReturnValue('XIAOJU_SURVEY_JWT_SECRET');
|
||||||
jest.spyOn(userService, 'getUserByUsername').mockResolvedValue(fakeUser);
|
jest.spyOn(authService, 'verifyToken').mockResolvedValue(fakeUser);
|
||||||
|
|
||||||
jest.spyOn(jwt, 'verify').mockReturnValue(fakeUser);
|
|
||||||
|
|
||||||
const result = await guard.canActivate(context as any);
|
const result = await guard.canActivate(context as any);
|
||||||
|
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
import { UserService } from '../modules/auth/services/user.service';
|
|
||||||
import { AuthenticationException } from '../exceptions/authException';
|
import { AuthenticationException } from '../exceptions/authException';
|
||||||
import { AuthService } from 'src/modules/auth/services/auth.service';
|
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class Authtication implements CanActivate {
|
export class Authtication implements CanActivate {
|
||||||
constructor(
|
constructor(private readonly authService: AuthService) {}
|
||||||
private readonly userService: UserService,
|
|
||||||
private readonly authService: AuthService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
|
@ -1,18 +1,33 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { AuthService } from '../services/auth.service';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { sign } from 'jsonwebtoken';
|
import * as jwt from 'jsonwebtoken';
|
||||||
|
import { User } from 'src/models/user.entity';
|
||||||
|
|
||||||
jest.mock('jsonwebtoken');
|
import { AuthService } from '../services/auth.service';
|
||||||
|
import { UserService } from '../services/user.service';
|
||||||
|
|
||||||
|
jest.mock('jsonwebtoken', () => {
|
||||||
|
return {
|
||||||
|
sign: jest.fn(),
|
||||||
|
verify: jest.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('AuthService', () => {
|
describe('AuthService', () => {
|
||||||
let service: AuthService;
|
let service: AuthService;
|
||||||
|
let userService: UserService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [AuthService],
|
imports: [ConfigModule],
|
||||||
|
providers: [
|
||||||
|
AuthService,
|
||||||
|
{ provide: UserService, useValue: { getUserByUsername: jest.fn() } },
|
||||||
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<AuthService>(AuthService);
|
service = module.get<AuthService>(AuthService);
|
||||||
|
userService = module.get<UserService>(UserService);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('generateToken', () => {
|
describe('generateToken', () => {
|
||||||
@ -25,9 +40,20 @@ describe('AuthService', () => {
|
|||||||
|
|
||||||
await service.generateToken(userData, tokenConfig);
|
await service.generateToken(userData, tokenConfig);
|
||||||
|
|
||||||
expect(sign).toHaveBeenCalledWith(userData, tokenConfig.secret, {
|
expect(jwt.sign).toHaveBeenCalledWith(userData, tokenConfig.secret, {
|
||||||
expiresIn: tokenConfig.expiresIn,
|
expiresIn: tokenConfig.expiresIn,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('verifyToken', () => {
|
||||||
|
it('should verifyToken succeed', async () => {
|
||||||
|
const token = 'mock token';
|
||||||
|
jest
|
||||||
|
.spyOn(userService, 'getUserByUsername')
|
||||||
|
.mockResolvedValue({} as User);
|
||||||
|
await service.verifyToken(token);
|
||||||
|
expect(userService.getUserByUsername).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
55
server/src/modules/file/__test/alioss.handler.spec.ts
Normal file
55
server/src/modules/file/__test/alioss.handler.spec.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { AliOssHandler } from '../services/uploadHandlers/alioss.handler';
|
||||||
|
|
||||||
|
describe('AliOssHandler', () => {
|
||||||
|
describe('upload', () => {
|
||||||
|
it('should upload a file and return the key', async () => {
|
||||||
|
const file = {
|
||||||
|
originalname: 'mockFileName.txt',
|
||||||
|
buffer: Buffer.from('mockFileContent'),
|
||||||
|
} as Express.Multer.File;
|
||||||
|
const mockPutResult = { name: 'mockFileName.txt' };
|
||||||
|
const mockClient = {
|
||||||
|
put: jest.fn().mockResolvedValue(mockPutResult),
|
||||||
|
};
|
||||||
|
const handler = new AliOssHandler({
|
||||||
|
client: mockClient,
|
||||||
|
useSSL: true,
|
||||||
|
isPrivateRead: true,
|
||||||
|
expiryTime: '1h',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handler.upload(file);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
key: expect.stringMatching(/\w+\.txt/),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUrl', () => {
|
||||||
|
it('should return the URL for the given key', () => {
|
||||||
|
const key = 'mockFilePath/mockFileName.txt';
|
||||||
|
const bucket = 'xiaojusurvey';
|
||||||
|
const accessKey = 'mockAccessKey';
|
||||||
|
const region = 'mockRegion';
|
||||||
|
const handler = new AliOssHandler({
|
||||||
|
accessKey,
|
||||||
|
secretKey: 'mockSecretKey',
|
||||||
|
bucket,
|
||||||
|
region,
|
||||||
|
endPoint: 'mockEndPoint',
|
||||||
|
useSSL: true,
|
||||||
|
isPrivateRead: true,
|
||||||
|
expiryTime: '1h',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = handler.getUrl(key);
|
||||||
|
const keyReg = new RegExp(key);
|
||||||
|
const signatureReg = new RegExp('Signature=');
|
||||||
|
const expiredReg = new RegExp('Expires=');
|
||||||
|
expect(keyReg.test(result)).toBe(true);
|
||||||
|
expect(signatureReg.test(result)).toBe(true);
|
||||||
|
expect(expiredReg.test(result)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
150
server/src/modules/file/__test/file.controller.spec.ts
Normal file
150
server/src/modules/file/__test/file.controller.spec.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
import { FileController } from '../controllers/file.controller';
|
||||||
|
import { FileService } from '../services/file.service';
|
||||||
|
import { uploadConfig, channels } from '../config/index';
|
||||||
|
|
||||||
|
import { AuthenticationException } from 'src/exceptions/authException';
|
||||||
|
import { HttpException } from 'src/exceptions/httpException';
|
||||||
|
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||||
|
import { User } from 'src/models/user.entity';
|
||||||
|
|
||||||
|
describe('FileController', () => {
|
||||||
|
let controller: FileController;
|
||||||
|
let fileService: FileService;
|
||||||
|
let authService: AuthService;
|
||||||
|
// let configService: ConfigService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forFeature(() => {
|
||||||
|
return {
|
||||||
|
...channels,
|
||||||
|
...uploadConfig,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
controllers: [FileController],
|
||||||
|
providers: [
|
||||||
|
FileService,
|
||||||
|
{
|
||||||
|
provide: AuthService,
|
||||||
|
useValue: {
|
||||||
|
verifyToken: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ConfigService,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<FileController>(FileController);
|
||||||
|
fileService = module.get<FileService>(FileService);
|
||||||
|
authService = module.get<AuthService>(AuthService);
|
||||||
|
// configService = module.get<ConfigService>(ConfigService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('upload', () => {
|
||||||
|
it('should upload a file', async () => {
|
||||||
|
const file: Express.Multer.File = {
|
||||||
|
fieldname: '',
|
||||||
|
originalname: '',
|
||||||
|
encoding: '',
|
||||||
|
mimetype: '',
|
||||||
|
size: 0,
|
||||||
|
stream: null,
|
||||||
|
destination: '',
|
||||||
|
filename: '',
|
||||||
|
path: '',
|
||||||
|
buffer: null,
|
||||||
|
};
|
||||||
|
const req = { headers: { authorization: 'Bearer mockToken' } };
|
||||||
|
const reqBody = { channel: 'upload' };
|
||||||
|
|
||||||
|
jest.spyOn(authService, 'verifyToken').mockResolvedValueOnce({} as User);
|
||||||
|
|
||||||
|
const mockUploadResult = { key: 'mockKey', url: 'mockUrl' };
|
||||||
|
jest.spyOn(fileService, 'upload').mockResolvedValueOnce(mockUploadResult);
|
||||||
|
|
||||||
|
const result = await controller.upload(file, req, reqBody);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
url: mockUploadResult.url,
|
||||||
|
key: mockUploadResult.key,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should upload failed without token', async () => {
|
||||||
|
const file: Express.Multer.File = {
|
||||||
|
fieldname: '',
|
||||||
|
originalname: '',
|
||||||
|
encoding: '',
|
||||||
|
mimetype: '',
|
||||||
|
size: 0,
|
||||||
|
stream: null,
|
||||||
|
destination: '',
|
||||||
|
filename: '',
|
||||||
|
path: '',
|
||||||
|
buffer: null,
|
||||||
|
};
|
||||||
|
const req = { headers: { authorization: '' } };
|
||||||
|
const reqBody = { channel: 'upload' };
|
||||||
|
|
||||||
|
await expect(controller.upload(file, req, reqBody)).rejects.toThrow(
|
||||||
|
AuthenticationException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should upload failed', async () => {
|
||||||
|
const file: Express.Multer.File = {
|
||||||
|
fieldname: '',
|
||||||
|
originalname: '',
|
||||||
|
encoding: '',
|
||||||
|
mimetype: '',
|
||||||
|
size: 0,
|
||||||
|
stream: null,
|
||||||
|
destination: '',
|
||||||
|
filename: '',
|
||||||
|
path: '',
|
||||||
|
buffer: null,
|
||||||
|
};
|
||||||
|
const req = { headers: { authorization: 'Bearer mockToken' } };
|
||||||
|
const reqBody = { channel: 'mockChannel' };
|
||||||
|
|
||||||
|
await expect(controller.upload(file, req, reqBody)).rejects.toThrow(
|
||||||
|
HttpException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generateGetUrl', () => {
|
||||||
|
it('should generate a URL for the given channel and key', async () => {
|
||||||
|
const reqBody = { channel: 'upload', key: 'mockKey' };
|
||||||
|
|
||||||
|
const mockUrl = 'mockGeneratedUrl';
|
||||||
|
jest.spyOn(fileService, 'getUrl').mockReturnValueOnce(mockUrl);
|
||||||
|
|
||||||
|
const result = await controller.generateGetUrl(reqBody);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
key: reqBody.key,
|
||||||
|
url: mockUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate a URL failed', async () => {
|
||||||
|
const reqBody = { channel: 'mockChannel', key: 'mockKey' };
|
||||||
|
|
||||||
|
await expect(controller.generateGetUrl(reqBody)).rejects.toThrow(
|
||||||
|
HttpException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
65
server/src/modules/file/__test/file.service.spec.ts
Normal file
65
server/src/modules/file/__test/file.service.spec.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
import { FileService } from '../services/file.service';
|
||||||
|
import { LocalHandler } from '../services/uploadHandlers/local.handler';
|
||||||
|
import { uploadConfig, channels } from '../config/index';
|
||||||
|
|
||||||
|
describe('FileService', () => {
|
||||||
|
let fileService: FileService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forFeature(() => {
|
||||||
|
return {
|
||||||
|
...channels,
|
||||||
|
...uploadConfig,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [FileService, ConfigService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
fileService = module.get<FileService>(FileService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('upload', () => {
|
||||||
|
it('should upload a file and return key and URL', async () => {
|
||||||
|
const configKey = 'mockConfigKey';
|
||||||
|
const file = {} as Express.Multer.File;
|
||||||
|
const pathPrefix = 'mockPathPrefix';
|
||||||
|
|
||||||
|
const mockKey = 'mockKey';
|
||||||
|
jest
|
||||||
|
.spyOn(LocalHandler.prototype, 'upload')
|
||||||
|
.mockResolvedValueOnce({ key: mockKey });
|
||||||
|
|
||||||
|
const mockUrl = 'mockUrl';
|
||||||
|
jest
|
||||||
|
.spyOn(LocalHandler.prototype, 'getUrl')
|
||||||
|
.mockResolvedValueOnce(mockUrl as never);
|
||||||
|
|
||||||
|
const result = await fileService.upload({ configKey, file, pathPrefix });
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
key: mockKey,
|
||||||
|
url: mockUrl,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUrl', () => {
|
||||||
|
it('should return URL for the given configKey and key', async () => {
|
||||||
|
const configKey = 'mockConfigKey';
|
||||||
|
const key = 'mockKey';
|
||||||
|
|
||||||
|
const mockUrl = 'mockUrl';
|
||||||
|
jest.spyOn(LocalHandler.prototype, 'getUrl').mockReturnValueOnce(mockUrl);
|
||||||
|
|
||||||
|
const result = fileService.getUrl({ configKey, key });
|
||||||
|
|
||||||
|
expect(result).toEqual(mockUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
32
server/src/modules/file/__test/local.handler.spec.ts
Normal file
32
server/src/modules/file/__test/local.handler.spec.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
import { LocalHandler } from '../services/uploadHandlers/local.handler';
|
||||||
|
|
||||||
|
describe('LocalHandler', () => {
|
||||||
|
describe('upload', () => {
|
||||||
|
it('should upload a file and return the key', async () => {
|
||||||
|
const file = {
|
||||||
|
originalname: `mockFileName.txt`,
|
||||||
|
buffer: Buffer.from('mockFileContent'),
|
||||||
|
} as Express.Multer.File;
|
||||||
|
const physicalRootPath = join(__dirname, 'tmp');
|
||||||
|
const handler = new LocalHandler({ physicalRootPath });
|
||||||
|
|
||||||
|
const result = await handler.upload(file);
|
||||||
|
expect(result).toEqual({
|
||||||
|
key: expect.stringMatching(/\w+\.txt/),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUrl', () => {
|
||||||
|
it('should return the URL for the given key', () => {
|
||||||
|
const key = 'mockFilePath/mockFileName.txt';
|
||||||
|
const physicalRootPath = join(__dirname, 'tmp');
|
||||||
|
const handler = new LocalHandler({ physicalRootPath });
|
||||||
|
|
||||||
|
const result = handler.getUrl(key);
|
||||||
|
|
||||||
|
expect(result).toBe(`/${key}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
75
server/src/modules/file/__test/minio.handler.spec.ts
Normal file
75
server/src/modules/file/__test/minio.handler.spec.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { Client } from 'minio';
|
||||||
|
|
||||||
|
import { MinIOHandler } from '../services/uploadHandlers/minio.handler';
|
||||||
|
|
||||||
|
import { HttpException } from 'src/exceptions/httpException';
|
||||||
|
|
||||||
|
describe('MinIOHandler', () => {
|
||||||
|
describe('upload', () => {
|
||||||
|
it('should upload a file and return the key', async () => {
|
||||||
|
const file = {
|
||||||
|
originalname: 'mockFileName.txt',
|
||||||
|
buffer: Buffer.from('mockFileContent'),
|
||||||
|
} as Express.Multer.File;
|
||||||
|
const mockPutObjectFn = jest.fn().mockResolvedValueOnce({});
|
||||||
|
const mockClient = {
|
||||||
|
putObject: mockPutObjectFn,
|
||||||
|
};
|
||||||
|
const handler = new MinIOHandler({
|
||||||
|
client: mockClient as unknown as Client,
|
||||||
|
useSSL: true,
|
||||||
|
isPrivateRead: true,
|
||||||
|
expiryTime: '1h',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handler.upload(file);
|
||||||
|
expect(result).toEqual({
|
||||||
|
key: expect.stringMatching('.txt'),
|
||||||
|
});
|
||||||
|
expect(mockPutObjectFn).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an HttpException if upload fails', async () => {
|
||||||
|
const file = {
|
||||||
|
originalname: 'mockFileName.txt',
|
||||||
|
buffer: Buffer.from('mockFileContent'),
|
||||||
|
} as Express.Multer.File;
|
||||||
|
const mockPutObjectFn = jest
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValueOnce(new Error('Upload failed'));
|
||||||
|
const mockClient = {
|
||||||
|
putObject: mockPutObjectFn,
|
||||||
|
};
|
||||||
|
const handler = new MinIOHandler({
|
||||||
|
client: mockClient as unknown as Client,
|
||||||
|
useSSL: true,
|
||||||
|
isPrivateRead: true,
|
||||||
|
expiryTime: '1h',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(handler.upload(file)).rejects.toThrow(HttpException);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUrl', () => {
|
||||||
|
it('should return the URL for the given key', async () => {
|
||||||
|
const key = 'mockFilePath/mockFileName.txt';
|
||||||
|
const handler = new MinIOHandler({
|
||||||
|
accessKey: 'mockAccessKey',
|
||||||
|
secretKey: 'mockSecretKey',
|
||||||
|
bucket: 'xiaojusurvey',
|
||||||
|
region: 'mockRegion',
|
||||||
|
endPoint: 'mockEndPoint',
|
||||||
|
useSSL: true,
|
||||||
|
isPrivateRead: true,
|
||||||
|
expiryTime: '1h',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handler.getUrl(key);
|
||||||
|
const keyReg = new RegExp(key);
|
||||||
|
const signatureReg = new RegExp('X-Amz-Signature=');
|
||||||
|
expect(keyReg.test(result)).toBe(true);
|
||||||
|
expect(signatureReg.test(result)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
102
server/src/modules/file/__test/qiniu.handler.spec.ts
Normal file
102
server/src/modules/file/__test/qiniu.handler.spec.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
// In your test file
|
||||||
|
import { QiniuHandler } from '../services/uploadHandlers/qiniu.handler';
|
||||||
|
import qiniu from 'qiniu';
|
||||||
|
|
||||||
|
jest.mock('qiniu', () => ({
|
||||||
|
auth: {
|
||||||
|
digest: {
|
||||||
|
Mac: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
conf: {
|
||||||
|
Config: jest.fn(),
|
||||||
|
Zone: jest.fn(),
|
||||||
|
},
|
||||||
|
form_up: {
|
||||||
|
FormUploader: jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
put: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(
|
||||||
|
(uploadToken, key, buffer, putExtra, callback) => {
|
||||||
|
callback && callback(null, null, { statusCode: 200 });
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
PutExtra: jest.fn(),
|
||||||
|
},
|
||||||
|
rs: {
|
||||||
|
PutPolicy: jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
uploadToken: jest.fn(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
BucketManager: jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
privateDownloadUrl: jest.fn().mockReturnValue(''),
|
||||||
|
publicDownloadUrl: jest.fn().mockReturnValue(''),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('QiniuHandler', () => {
|
||||||
|
describe('upload', () => {
|
||||||
|
it('should upload a file and return the key', async () => {
|
||||||
|
const file = {
|
||||||
|
originalname: 'mockFileName.txt',
|
||||||
|
buffer: Buffer.from('mockFileContent'),
|
||||||
|
} as Express.Multer.File;
|
||||||
|
|
||||||
|
const mockMacInstance = {
|
||||||
|
sign: jest.fn(),
|
||||||
|
};
|
||||||
|
jest
|
||||||
|
.spyOn(qiniu.auth.digest, 'Mac')
|
||||||
|
.mockImplementation(() => mockMacInstance as any);
|
||||||
|
|
||||||
|
// Create a new instance of QiniuHandler
|
||||||
|
const handler = new QiniuHandler({
|
||||||
|
accessKey: 'mockAccessKey',
|
||||||
|
secretKey: 'mockSecretKey',
|
||||||
|
bucket: 'mockBucket',
|
||||||
|
endPoint: 'mockEndPoint',
|
||||||
|
useSSL: true,
|
||||||
|
isPrivateRead: true,
|
||||||
|
expiryTime: '1h',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handler.upload(file);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
key: expect.stringMatching('.txt'),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(qiniu.auth.digest.Mac).toHaveBeenCalledWith(
|
||||||
|
'mockAccessKey',
|
||||||
|
'mockSecretKey',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(qiniu.form_up.FormUploader).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUrl', () => {
|
||||||
|
it('should return the URL for the given key', async () => {
|
||||||
|
const key = 'mockFilePath/mockFileName.txt';
|
||||||
|
const handler = new QiniuHandler({
|
||||||
|
accessKey: 'mockAccessKey',
|
||||||
|
secretKey: 'mockSecretKey',
|
||||||
|
bucket: 'xiaojusurvey',
|
||||||
|
endPoint: 'mockEndPoint',
|
||||||
|
useSSL: true,
|
||||||
|
isPrivateRead: true,
|
||||||
|
expiryTime: '1h',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handler.getUrl(key);
|
||||||
|
expect(typeof result).toBe('string');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -24,26 +24,6 @@ export class FileController {
|
|||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private getPathPrefix(fileKeyPrefix: string, data: Record<string, string>) {
|
|
||||||
const regex = /\{([^}]*)\}/g;
|
|
||||||
let matches;
|
|
||||||
const keys = [];
|
|
||||||
while ((matches = regex.exec(fileKeyPrefix)) !== null) {
|
|
||||||
keys.push(matches[1]);
|
|
||||||
}
|
|
||||||
let result = fileKeyPrefix;
|
|
||||||
for (const key of keys) {
|
|
||||||
if (!data[key]) {
|
|
||||||
throw new HttpException(
|
|
||||||
`参数有误:${data[key]}`,
|
|
||||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
result = result.replace(new RegExp(`{${key}}`), data[key]);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('upload')
|
@Post('upload')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
@ -56,12 +36,12 @@ export class FileController {
|
|||||||
|
|
||||||
if (!channel || !this.configService.get<string>(channel)) {
|
if (!channel || !this.configService.get<string>(channel)) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
`参数有误:${channel}`,
|
`参数有误channel不正确:${reqBody.channel}`,
|
||||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const configKey = this.configService.get<string>(channel);
|
const configKey = this.configService.get<string>(channel);
|
||||||
const needAuth = this.configService.get<boolean>(configKey);
|
const needAuth = this.configService.get<boolean>(`${configKey}.NEED_AUTH`);
|
||||||
if (needAuth) {
|
if (needAuth) {
|
||||||
const token = req.headers.authorization?.split(' ')[1];
|
const token = req.headers.authorization?.split(' ')[1];
|
||||||
|
|
||||||
|
@ -10,10 +10,6 @@ import { LocalHandler } from './uploadHandlers/local.handler';
|
|||||||
export class FileService {
|
export class FileService {
|
||||||
constructor(private readonly configService: ConfigService) {}
|
constructor(private readonly configService: ConfigService) {}
|
||||||
|
|
||||||
private getConfig<T>(channel, key) {
|
|
||||||
return this.configService.get<T>(channel + '_' + key);
|
|
||||||
}
|
|
||||||
|
|
||||||
async upload({
|
async upload({
|
||||||
configKey,
|
configKey,
|
||||||
file,
|
file,
|
||||||
|
@ -11,6 +11,7 @@ export class AliOssHandler implements FileUploadHandler {
|
|||||||
isPrivateRead: boolean;
|
isPrivateRead: boolean;
|
||||||
expiryTime: string;
|
expiryTime: string;
|
||||||
constructor({
|
constructor({
|
||||||
|
client,
|
||||||
accessKey,
|
accessKey,
|
||||||
secretKey,
|
secretKey,
|
||||||
bucket,
|
bucket,
|
||||||
@ -19,13 +20,25 @@ export class AliOssHandler implements FileUploadHandler {
|
|||||||
useSSL,
|
useSSL,
|
||||||
isPrivateRead,
|
isPrivateRead,
|
||||||
expiryTime,
|
expiryTime,
|
||||||
|
}: {
|
||||||
|
client?: OSS;
|
||||||
|
accessKey?: string;
|
||||||
|
secretKey?: string;
|
||||||
|
bucket?: string;
|
||||||
|
region?: string;
|
||||||
|
endPoint?: string;
|
||||||
|
useSSL?: boolean;
|
||||||
|
isPrivateRead?: boolean;
|
||||||
|
expiryTime?: string;
|
||||||
}) {
|
}) {
|
||||||
const client = new OSS({
|
if (!client) {
|
||||||
|
client = new OSS({
|
||||||
region,
|
region,
|
||||||
accessKeyId: accessKey,
|
accessKeyId: accessKey,
|
||||||
accessKeySecret: secretKey,
|
accessKeySecret: secretKey,
|
||||||
bucket,
|
bucket,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.endPoint = endPoint;
|
this.endPoint = endPoint;
|
||||||
this.useSSL = useSSL;
|
this.useSSL = useSSL;
|
||||||
|
@ -14,6 +14,7 @@ export class MinIOHandler implements FileUploadHandler {
|
|||||||
expiryTime: string;
|
expiryTime: string;
|
||||||
bucket: string;
|
bucket: string;
|
||||||
constructor({
|
constructor({
|
||||||
|
client,
|
||||||
accessKey,
|
accessKey,
|
||||||
secretKey,
|
secretKey,
|
||||||
bucket,
|
bucket,
|
||||||
@ -22,8 +23,19 @@ export class MinIOHandler implements FileUploadHandler {
|
|||||||
useSSL,
|
useSSL,
|
||||||
isPrivateRead,
|
isPrivateRead,
|
||||||
expiryTime,
|
expiryTime,
|
||||||
|
}: {
|
||||||
|
client?: Client;
|
||||||
|
accessKey?: string;
|
||||||
|
secretKey?: string;
|
||||||
|
bucket?: string;
|
||||||
|
region?: string;
|
||||||
|
endPoint?: string;
|
||||||
|
useSSL?: boolean;
|
||||||
|
isPrivateRead?: boolean;
|
||||||
|
expiryTime?: string;
|
||||||
}) {
|
}) {
|
||||||
const client = new Client({
|
if (!client) {
|
||||||
|
client = new Client({
|
||||||
endPoint,
|
endPoint,
|
||||||
accessKey,
|
accessKey,
|
||||||
secretKey,
|
secretKey,
|
||||||
@ -31,6 +43,7 @@ export class MinIOHandler implements FileUploadHandler {
|
|||||||
useSSL,
|
useSSL,
|
||||||
pathStyle: true,
|
pathStyle: true,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.endPoint = endPoint;
|
this.endPoint = endPoint;
|
||||||
this.useSSL = useSSL;
|
this.useSSL = useSSL;
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
export const replaceFileKey = (
|
|
||||||
originStr: string,
|
|
||||||
arr: Array<{ key; value }>,
|
|
||||||
) => {
|
|
||||||
let retStr = originStr;
|
|
||||||
for (const { key, value } of arr) {
|
|
||||||
if (value) {
|
|
||||||
retStr = retStr.replace(new RegExp(`{${key}}`), value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return retStr;
|
|
||||||
};
|
|
@ -5,7 +5,6 @@ import { MessagePushingTaskService } from '../services/messagePushingTask.servic
|
|||||||
import { CreateMessagePushingTaskDto } from '../dto/createMessagePushingTask.dto';
|
import { CreateMessagePushingTaskDto } from '../dto/createMessagePushingTask.dto';
|
||||||
import { QueryMessagePushingTaskListDto } from '../dto/queryMessagePushingTaskList.dto';
|
import { QueryMessagePushingTaskListDto } from '../dto/queryMessagePushingTaskList.dto';
|
||||||
import { HttpException } from 'src/exceptions/httpException';
|
import { HttpException } from 'src/exceptions/httpException';
|
||||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
|
||||||
import {
|
import {
|
||||||
MESSAGE_PUSHING_HOOK,
|
MESSAGE_PUSHING_HOOK,
|
||||||
MESSAGE_PUSHING_TYPE,
|
MESSAGE_PUSHING_TYPE,
|
||||||
@ -16,6 +15,7 @@ import { UserService } from 'src/modules/auth/services/user.service';
|
|||||||
|
|
||||||
import { UpdateMessagePushingTaskDto } from '../dto/updateMessagePushingTask.dto';
|
import { UpdateMessagePushingTaskDto } from '../dto/updateMessagePushingTask.dto';
|
||||||
import { ObjectId } from 'mongodb';
|
import { ObjectId } from 'mongodb';
|
||||||
|
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||||
|
|
||||||
describe('MessagePushingTaskController', () => {
|
describe('MessagePushingTaskController', () => {
|
||||||
let controller: MessagePushingTaskController;
|
let controller: MessagePushingTaskController;
|
||||||
@ -51,6 +51,14 @@ describe('MessagePushingTaskController', () => {
|
|||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: AuthService,
|
||||||
|
useClass: jest.fn().mockImplementation(() => ({
|
||||||
|
verifyToken() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
@ -137,9 +145,7 @@ describe('MessagePushingTaskController', () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
controller.findAll(req, queryDto as QueryMessagePushingTaskListDto),
|
controller.findAll(req, queryDto as QueryMessagePushingTaskListDto),
|
||||||
).rejects.toThrow(
|
).rejects.toThrow(HttpException);
|
||||||
new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR),
|
|
||||||
);
|
|
||||||
expect(service.findAll).toHaveBeenCalledTimes(0);
|
expect(service.findAll).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -65,7 +65,6 @@ describe('MessagePushingTaskService', () => {
|
|||||||
savedTask.type = MESSAGE_PUSHING_TYPE.HTTP;
|
savedTask.type = MESSAGE_PUSHING_TYPE.HTTP;
|
||||||
savedTask.pushAddress = 'http://example.com';
|
savedTask.pushAddress = 'http://example.com';
|
||||||
savedTask.triggerHook = MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED;
|
savedTask.triggerHook = MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED;
|
||||||
savedTask.surveys = ['surveyId1', 'surveyId2'];
|
|
||||||
|
|
||||||
const mockOwnerId = '66028642292c50f8b71a9eee';
|
const mockOwnerId = '66028642292c50f8b71a9eee';
|
||||||
|
|
||||||
|
@ -37,10 +37,7 @@ export class CreateMessagePushingTaskDto {
|
|||||||
triggerHook: Joi.string()
|
triggerHook: Joi.string()
|
||||||
.allow(null)
|
.allow(null)
|
||||||
.default(MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED),
|
.default(MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED),
|
||||||
surveys: Joi.array()
|
surveys: Joi.array().items(Joi.string()).allow(null).default([]),
|
||||||
.items(Joi.string().required())
|
|
||||||
.allow(null)
|
|
||||||
.default([]),
|
|
||||||
}).validateAsync(data);
|
}).validateAsync(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
|||||||
import { Authtication } from 'src/guards/authtication';
|
import { Authtication } from 'src/guards/authtication';
|
||||||
import { UserService } from 'src/modules/auth/services/user.service';
|
import { UserService } from 'src/modules/auth/services/user.service';
|
||||||
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
||||||
|
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||||
|
|
||||||
jest.mock('../services/dataStatistic.service');
|
jest.mock('../services/dataStatistic.service');
|
||||||
jest.mock('../services/surveyMeta.service');
|
jest.mock('../services/surveyMeta.service');
|
||||||
@ -45,6 +46,14 @@ describe('DataStatisticController', () => {
|
|||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: AuthService,
|
||||||
|
useClass: jest.fn().mockImplementation(() => ({
|
||||||
|
varifytoken() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
import { SurveyHistoryController } from '../controllers/surveyHistory.controller';
|
import { SurveyHistoryController } from '../controllers/surveyHistory.controller';
|
||||||
import { SurveyHistoryService } from '../services/surveyHistory.service';
|
import { SurveyHistoryService } from '../services/surveyHistory.service';
|
||||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||||
|
|
||||||
import { UserService } from 'src/modules/auth/services/user.service';
|
import { UserService } from 'src/modules/auth/services/user.service';
|
||||||
import { Authtication } from 'src/guards/authtication';
|
import { Authtication } from 'src/guards/authtication';
|
||||||
|
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
describe('SurveyHistoryController', () => {
|
describe('SurveyHistoryController', () => {
|
||||||
let controller: SurveyHistoryController;
|
let controller: SurveyHistoryController;
|
||||||
@ -43,6 +45,14 @@ describe('SurveyHistoryController', () => {
|
|||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: AuthService,
|
||||||
|
useClass: jest.fn().mockImplementation(() => ({
|
||||||
|
verifyToken() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user