refactor: serverless module

This commit is contained in:
Innei
2022-03-11 11:12:09 +08:00
parent 5d88d6333f
commit 0fc0b6f84b
19 changed files with 528 additions and 360 deletions

View File

@@ -110,6 +110,7 @@ docker-compose up -d
│ ├── say
│ ├── search
| ├── snippet
| ├── serverless
│ ├── sitemap
│ ├── tool
│ └── user

View File

@@ -33,6 +33,7 @@ import { PTYModule } from './modules/pty/pty.module'
import { RecentlyModule } from './modules/recently/recently.module'
import { SayModule } from './modules/say/say.module'
import { SearchModule } from './modules/search/search.module'
import { ServerlessModule } from './modules/serverless/serverless.module'
import { SitemapModule } from './modules/sitemap/sitemap.module'
import { SnippetModule } from './modules/snippet/snippet.module'
import { ToolModule } from './modules/tool/tool.module'
@@ -70,6 +71,7 @@ import { LoggerModule } from './processors/logger/logger.module'
RecentlyModule,
SayModule,
SearchModule,
ServerlessModule,
SitemapModule,
SnippetModule,
ToolModule,

View File

@@ -12,9 +12,9 @@ import { AdminEventsGateway } from '~/processors/gateway/admin/events.gateway'
import { EventTypes } from '~/processors/gateway/events.types'
import { WebEventsGateway } from '~/processors/gateway/web/events.gateway'
import { PagerDto } from '~/shared/dto/pager.dto'
import { createMockedContextResponse } from '../snippet/mock-response.util'
import { createMockedContextResponse } from '../serverless/mock-response.util'
import { ServerlessService } from '../serverless/serverless.service'
import { SnippetModel, SnippetType } from '../snippet/snippet.model'
import { SnippetService } from '../snippet/snippet.service'
@Controller('debug')
export class DebugController {
@@ -22,7 +22,7 @@ export class DebugController {
private readonly webEvent: WebEventsGateway,
private readonly adminEvent: AdminEventsGateway,
private readonly snippetService: SnippetService,
private readonly serverlessService: ServerlessService,
) {}
@Get('qs')
async qs(@Query() query: PagerDto) {
@@ -58,7 +58,7 @@ export class DebugController {
model.private = false
model.type = SnippetType.Function
NotFoundException
return await this.snippetService.injectContextIntoServerlessFunctionAndCall(
return await this.serverlessService.injectContextIntoServerlessFunctionAndCall(
model,
{ req, res: createMockedContextResponse() },
)

View File

@@ -1,9 +1,9 @@
import { Module } from '@nestjs/common'
import { SnippetModule } from '../snippet/snippet.module'
import { ServerlessModule } from '../serverless/serverless.module'
import { DebugController } from './debug.controller'
@Module({
controllers: [DebugController],
imports: [SnippetModule],
imports: [ServerlessModule],
})
export class DebugModule {}

View File

@@ -0,0 +1,48 @@
import {
Controller,
ForbiddenException,
Get,
NotFoundException,
Param,
Request,
} from '@nestjs/common'
import type { FastifyRequest } from 'fastify'
import { HTTPDecorators } from '~/common/decorator/http.decorator'
import { ApiName } from '~/common/decorator/openapi.decorator'
import { IsMaster } from '~/common/decorator/role.decorator'
import { createMockedContextResponse } from './mock-response.util'
import { ServerlessReferenceDto } from './serverless.dto'
import { ServerlessService } from './serverless.service'
@ApiName
@Controller('serverless')
export class ServerlessController {
constructor(private readonly serverlessService: ServerlessService) {}
@Get('/:reference/:name')
@HTTPDecorators.Bypass
async runServerlessFunction(
@Param() param: ServerlessReferenceDto,
@IsMaster() isMaster: boolean,
@Request() req: FastifyRequest,
) {
const { name, reference } = param
const snippet = await this.serverlessService.model.findOne({
name,
reference,
})
if (!snippet) {
throw new NotFoundException('snippet is not found')
}
if (snippet.private && !isMaster) {
throw new ForbiddenException('no permission to run this function')
}
return this.serverlessService.injectContextIntoServerlessFunctionAndCall(
snippet,
{ req, res: createMockedContextResponse() },
)
}
}

View File

@@ -0,0 +1,11 @@
import { IsNotEmpty, IsString } from 'class-validator'
export class ServerlessReferenceDto {
@IsString()
@IsNotEmpty()
reference: string
@IsString()
@IsNotEmpty()
name: string
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common'
import { ServerlessController } from './serverless.controller'
import { ServerlessService } from './serverless.service'
@Module({
controllers: [ServerlessController],
providers: [ServerlessService],
exports: [ServerlessService],
})
export class ServerlessModule {}

View File

@@ -151,4 +151,5 @@ And other global api is all banned.
- [ ] handle safeEval throw
- [ ] MongoDb inject (can access db)
- [ ] set Content-Type
- [ ] ESM AST Parser
- [ ] ESM AST Parser
- [ ] Cron to clean require cache

View File

@@ -0,0 +1,241 @@
import { Injectable, Logger } from '@nestjs/common'
import { isURL } from 'class-validator'
import fs, { mkdir, stat } from 'fs/promises'
import { cloneDeep } from 'lodash'
import { InjectModel } from 'nestjs-typegoose'
import { join } from 'path'
import { nextTick } from 'process'
import { DATA_DIR, NODE_REQUIRE_PATH } from '~/constants/path.constant'
import { AssetService } from '~/processors/helper/helper.asset.service'
import { HttpService } from '~/processors/helper/helper.http.service'
import { UniqueArray } from '~/ts-hepler/unique'
import { safePathJoin } from '~/utils'
import { safeEval } from '~/utils/safe-eval.util'
import { isBuiltinModule } from '~/utils/sys.util'
import type PKG from '../../../package.json'
import { SnippetModel } from '../snippet/snippet.model'
import {
FunctionContextRequest,
FunctionContextResponse,
} from './function.types'
@Injectable()
export class ServerlessService {
constructor(
@InjectModel(SnippetModel)
private readonly snippetModel: MongooseModel<SnippetModel>,
private readonly assetService: AssetService,
private readonly httpService: HttpService,
) {
nextTick(() => {
// Add /includes/plugin to the path, also note that we need to support
// `require('../hello.js')`. We can do that by adding /includes/plugin/a,
// /includes/plugin/a/b, etc.. to the list
mkdir(NODE_REQUIRE_PATH, { recursive: true }).then(async () => {
const pkgPath = join(NODE_REQUIRE_PATH, 'package.json')
const isPackageFileExist = await stat(pkgPath)
.then(() => true)
.catch(() => false)
if (!isPackageFileExist) {
await fs.writeFile(
pkgPath,
JSON.stringify({ name: 'modules' }, null, 2),
)
}
})
module.paths.push(NODE_REQUIRE_PATH)
// if (isDev) {
// console.log(module.paths)
// }
})
}
public get model() {
return this.snippetModel
}
async injectContextIntoServerlessFunctionAndCall(
model: SnippetModel,
context: { req: FunctionContextRequest; res: FunctionContextResponse },
) {
const { raw: functionString } = model
const logger = new Logger('ServerlessFunction/' + model.name)
const document = await this.model.findById(model.id)
const globalContext = {
context: {
// inject app req, res
...context,
...context.res,
query: context.req.query,
headers: context.req.headers,
params: context.req.params,
model,
document,
name: model.name,
reference: model.reference,
// TODO
// write file to asset
writeAsset: async (
path: string,
data: any,
options: Parameters<typeof fs.writeFile>[2],
) => {
return await this.assetService.writeUserCustomAsset(
safePathJoin(path),
data,
options,
)
},
// read file to asset
readAsset: async (
path: string,
options: Parameters<typeof fs.readFile>[1],
) => {
return await this.assetService.getAsset(safePathJoin(path), options)
},
},
// inject global
__dirname: DATA_DIR,
// inject some zx utils
fetch,
// inject Global API
Buffer,
// inject logger
console: logger,
logger,
require: (() => {
const __require = (id: string) => {
const module = require(id)
return cloneDeep(module)
}
const __requireNoCache = (id: string) => {
delete require.cache[require.resolve(id)]
const module = require(id)
return cloneDeep(module)
}
async function $require(
this: ServerlessService,
id: string,
useCache = true,
) {
if (!id || typeof id !== 'string') {
throw new Error('require id is not valid')
}
// 1. if is remote module
if (
isURL(id, { protocols: ['http', 'https'], require_protocol: true })
) {
const text = await this.httpService.getAndCacheRequest(id)
return await safeEval(`${text}; return module.exports`, {
exports: {},
module: {
exports: null,
},
})
}
// 2. if application third part lib
const allowedThirdPartLibs: UniqueArray<
(keyof typeof PKG.dependencies)[]
> = [
'algoliasearch',
'axios-retry',
'axios',
'class-transformer',
'class-validator',
'dayjs',
'ejs',
'html-minifier',
'image-size',
'isbot',
'js-yaml',
'jsdom',
'jszip',
'lodash',
'marked',
'nanoid',
'qs',
'rxjs',
'snakecase-keys',
'ua-parser-js',
'xss',
]
const trustPackagePrefixes = ['@innei/', '@mx-space/', 'mx-function-']
if (
allowedThirdPartLibs.includes(id as any) ||
trustPackagePrefixes.some((prefix) => id.startsWith(prefix))
) {
return useCache ? __require(id) : __requireNoCache(id)
}
// 3. mock built-in module
const mockModules = {
fs: {
writeFile: globalContext.context.writeAsset,
readFile: globalContext.context.readAsset,
},
}
if (Object.keys(mockModules).includes(id)) {
return mockModules[id]
}
// fin. is built-in module
const module = isBuiltinModule(id, [
'fs',
'os',
'child_process',
'sys',
])
if (!module) {
throw new Error(`cannot require ${id}`)
} else {
return __require(id)
}
}
return $require.bind(this)
})(),
process: {
env: Object.freeze({ ...process.env }),
nextTick: process.nextTick,
},
}
return await safeEval(
`${functionString}; return handler(context, require)`,
{ ...globalContext, global: globalContext, globalThis: globalContext },
)
}
async isValidServerlessFunction(raw: string) {
try {
return safeEval(`
${raw}
// 验证 handler 是否存在并且是函数
return typeof handler === 'function'
`)
} catch (e) {
console.error(e.message)
return false
}
}
}

View File

@@ -1,4 +1,5 @@
import {
BadRequestException,
Body,
Controller,
Delete,
@@ -8,9 +9,7 @@ import {
Post,
Put,
Query,
Request,
} from '@nestjs/common'
import type { FastifyRequest } from 'fastify'
import { Auth } from '~/common/decorator/auth.decorator'
import { HttpCache } from '~/common/decorator/cache.decorator'
import { HTTPDecorators } from '~/common/decorator/http.decorator'
@@ -19,7 +18,6 @@ import { IsMaster } from '~/common/decorator/role.decorator'
import { MongoIdDto } from '~/shared/dto/id.dto'
import { PagerDto } from '~/shared/dto/pager.dto'
import { transformDataToPaginate } from '~/utils/transfrom.util'
import { createMockedContextResponse } from './mock-response.util'
import { SnippetModel, SnippetType } from './snippet.model'
import { SnippetService } from './snippet.service'
@@ -68,8 +66,6 @@ export class SnippetController {
@Param('name') name: string,
@Param('reference') reference: string,
@IsMaster() isMaster: boolean,
@Request() req: FastifyRequest,
) {
if (typeof name !== 'string') {
throw new ForbiddenException('name should be string')
@@ -87,11 +83,12 @@ export class SnippetController {
if (snippet.type !== SnippetType.Function) {
return this.snippetService.attachSnippet(snippet).then((res) => res.data)
}
// run serverless function
return this.snippetService.injectContextIntoServerlessFunctionAndCall(
snippet,
{ req, res: createMockedContextResponse() },
)
if (snippet.type === SnippetType.Function) {
throw new BadRequestException(
'this snippet should run in serverless function scope.',
)
}
}
@Put('/:id')

View File

@@ -49,7 +49,7 @@ export class SnippetModel extends BaseModel {
@prop({ require: true, trim: true })
@IsNotEmpty()
@Matches(/^[a-zA-Z0-9_]{1,30}$/, {
@Matches(/^[a-zA-Z0-9_-]{1,30}$/, {
message: 'name 只能使用英文字母和数字下划线且不超过 30 个字符',
})
name: string

View File

@@ -1,7 +1,8 @@
/**
* 数据配置区块
*/
import { Module } from '@nestjs/common'
import { forwardRef, Module } from '@nestjs/common'
import { ServerlessModule } from '../serverless/serverless.module'
import { SnippetController } from './snippet.controller'
import { SnippetService } from './snippet.service'
@@ -9,5 +10,6 @@ import { SnippetService } from './snippet.service'
controllers: [SnippetController],
exports: [SnippetService],
providers: [SnippetService],
imports: [forwardRef(() => ServerlessModule)],
})
export class SnippetModule {}

View File

@@ -1,28 +1,13 @@
import {
BadRequestException,
forwardRef,
Inject,
Injectable,
Logger,
NotFoundException,
} from '@nestjs/common'
import { isURL } from 'class-validator'
import fs, { mkdir, stat } from 'fs/promises'
import { load } from 'js-yaml'
import { cloneDeep } from 'lodash'
import { InjectModel } from 'nestjs-typegoose'
import { join } from 'path'
import { nextTick } from 'process'
import type PKG from '~/../package.json'
import { DATA_DIR, NODE_REQUIRE_PATH } from '~/constants/path.constant'
import { AssetService } from '~/processors/helper/helper.asset.service'
import { HttpService } from '~/processors/helper/helper.http.service'
import { UniqueArray } from '~/ts-hepler/unique'
import { safePathJoin } from '~/utils'
import { safeEval } from '~/utils/safe-eval.util'
import { isBuiltinModule } from '~/utils/sys.util'
import {
FunctionContextRequest,
FunctionContextResponse,
} from './function.types'
import { ServerlessService } from '../serverless/serverless.service'
import { SnippetModel, SnippetType } from './snippet.model'
@Injectable()
@@ -30,34 +15,9 @@ export class SnippetService {
constructor(
@InjectModel(SnippetModel)
private readonly snippetModel: MongooseModel<SnippetModel>,
private readonly assetService: AssetService,
private readonly httpService: HttpService,
) {
nextTick(() => {
// Add /includes/plugin to the path, also note that we need to support
// `require('../hello.js')`. We can do that by adding /includes/plugin/a,
// /includes/plugin/a/b, etc.. to the list
mkdir(NODE_REQUIRE_PATH, { recursive: true }).then(async () => {
const pkgPath = join(NODE_REQUIRE_PATH, 'package.json')
const isPackageFileExist = await stat(pkgPath)
.then(() => true)
.catch(() => false)
if (!isPackageFileExist) {
await fs.writeFile(
pkgPath,
JSON.stringify({ name: 'modules' }, null, 2),
)
}
})
module.paths.push(NODE_REQUIRE_PATH)
// if (isDev) {
// console.log(module.paths)
// }
})
}
@Inject(forwardRef(() => ServerlessService))
private readonly serverlessService: ServerlessService,
) {}
get model() {
return this.snippetModel
@@ -107,7 +67,9 @@ export class SnippetService {
break
}
case SnippetType.Function: {
const isValid = await this.isValidServerlessFunction(model.raw)
const isValid = await this.serverlessService.isValidServerlessFunction(
model.raw,
)
if (!isValid) {
throw new BadRequestException('serverless function is not valid')
}
@@ -121,189 +83,6 @@ export class SnippetService {
}
}
async injectContextIntoServerlessFunctionAndCall(
model: SnippetModel,
context: { req: FunctionContextRequest; res: FunctionContextResponse },
) {
const { raw: functionString } = model
const logger = new Logger('ServerlessFunction/' + model.name)
const document = await this.model.findById(model.id)
const globalContext = {
context: {
// inject app req, res
...context,
...context.res,
query: context.req.query,
headers: context.req.headers,
params: context.req.params,
model,
document,
name: model.name,
reference: model.reference,
// TODO
// write file to asset
writeAsset: async (
path: string,
data: any,
options: Parameters<typeof fs.writeFile>[2],
) => {
return await this.assetService.writeUserCustomAsset(
safePathJoin(path),
data,
options,
)
},
// read file to asset
readAsset: async (
path: string,
options: Parameters<typeof fs.readFile>[1],
) => {
return await this.assetService.getAsset(safePathJoin(path), options)
},
},
// inject global
__dirname: DATA_DIR,
// inject some zx utils
fetch,
// inject Global API
Buffer,
// inject logger
console: logger,
logger,
require: (() => {
const __require = (id: string) => {
const module = require(id)
return cloneDeep(module)
}
const __requireNoCache = (id: string) => {
delete require.cache[require.resolve(id)]
const module = require(id)
delete require.cache[require.resolve(id)]
return cloneDeep(module)
}
async function $require(
this: SnippetService,
id: string,
useCache = true,
) {
if (!id || typeof id !== 'string') {
throw new Error('require id is not valid')
}
// 1. if is remote module
if (
isURL(id, { protocols: ['http', 'https'], require_protocol: true })
) {
const text = await this.httpService.getAndCacheRequest(id)
return await safeEval(`${text}; return module.exports`, {
exports: {},
module: {
exports: null,
},
})
}
// 2. if application third part lib
const allowedThirdPartLibs: UniqueArray<
(keyof typeof PKG.dependencies)[]
> = [
'algoliasearch',
'axios-retry',
'axios',
'class-transformer',
'class-validator',
'dayjs',
'ejs',
'html-minifier',
'image-size',
'isbot',
'js-yaml',
'jsdom',
'jszip',
'lodash',
'marked',
'nanoid',
'qs',
'rxjs',
'snakecase-keys',
'ua-parser-js',
'xss',
]
const trustPackagePrefixes = ['@innei/', '@mx-space/', 'mx-function-']
if (
allowedThirdPartLibs.includes(id as any) ||
trustPackagePrefixes.some((prefix) => id.startsWith(prefix))
) {
return useCache ? __require(id) : __requireNoCache(id)
}
// 3. mock built-in module
const mockModules = {
fs: {
writeFile: globalContext.context.writeAsset,
readFile: globalContext.context.readAsset,
},
}
if (Object.keys(mockModules).includes(id)) {
return mockModules[id]
}
// fin. is built-in module
const module = isBuiltinModule(id, [
'fs',
'os',
'child_process',
'sys',
])
if (!module) {
throw new Error(`cannot require ${id}`)
} else {
return __require(id)
}
}
return $require.bind(this)
})(),
process: {
env: Object.freeze({ ...process.env }),
nextTick: process.nextTick,
},
}
return await safeEval(
`${functionString}; return handler(context, require)`,
{ ...globalContext, global: globalContext, globalThis: globalContext },
)
}
async isValidServerlessFunction(raw: string) {
try {
return safeEval(`
${raw}
// 验证 handler 是否存在并且是函数
return typeof handler === 'function'
`)
} catch (e) {
console.error(e.message)
return false
}
}
async getSnippetById(id: string) {
const doc = await this.model.findById(id).lean()
if (!doc) {

View File

@@ -0,0 +1,136 @@
import { Test } from '@nestjs/testing'
import { getModelForClass } from '@typegoose/typegoose'
import { getModelToken } from 'nestjs-typegoose'
import { dbHelper } from 'test/helper/db-mock.helper'
import { createMockedContextResponse } from '~/modules/serverless/mock-response.util'
import { ServerlessService } from '~/modules/serverless/serverless.service'
import { SnippetModel, SnippetType } from '~/modules/snippet/snippet.model'
import { CacheService } from '~/processors/cache/cache.service'
import { AssetService } from '~/processors/helper/helper.asset.service'
import { HttpService } from '~/processors/helper/helper.http.service'
describe('test serverless function service', () => {
let service: ServerlessService
beforeAll(async () => {
await dbHelper.connect()
const moduleRef = Test.createTestingModule({
providers: [
ServerlessService,
HttpService,
AssetService,
{
provide: CacheService,
useValue: {},
},
{
provide: getModelToken('SnippetModel'),
useValue: getModelForClass(SnippetModel),
},
],
})
const app = await moduleRef.compile()
await app.init()
service = app.get(ServerlessService)
})
describe('run serverless function', () => {
test('case-1', async () => {
const model = new SnippetModel()
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
type: SnippetType.Function,
raw: async function handler(context, require) {
return 1 + 1
}.toString(),
})
const data = await service.injectContextIntoServerlessFunctionAndCall(
model,
{ req: {} as any, res: {} as any },
)
expect(data).toBe(2)
})
test('case-2: require built-in module', async () => {
const model = new SnippetModel()
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
type: SnippetType.Function,
raw: async function handler(context, require) {
return (await require('path')).join('1', '1')
}.toString(),
})
const data = await service.injectContextIntoServerlessFunctionAndCall(
model,
{ req: {} as any, res: {} as any },
)
expect(data).toBe('1/1')
})
test('case-3: require extend module', async () => {
const model = new SnippetModel()
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
type: SnippetType.Function,
raw: async function handler(context, require) {
return (await require('axios')).get.toString()
}.toString(),
})
const data = await service.injectContextIntoServerlessFunctionAndCall(
model,
{ req: {} as any, res: {} as any },
)
expect(data).toBeDefined()
})
test('case-4: require ban module', async () => {
const model = new SnippetModel()
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
type: SnippetType.Function,
raw: async function handler(context, require) {
return await require('os')
}.toString(),
})
expect(
service.injectContextIntoServerlessFunctionAndCall(model, {
req: {} as any,
res: {} as any,
}),
).rejects.toThrow()
})
test('case-5: require ban extend module', async () => {
const model = new SnippetModel()
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
type: SnippetType.Function,
raw: async function handler(context, require) {
return await require('@nestjs/core')
}.toString(),
})
expect(
service.injectContextIntoServerlessFunctionAndCall(model, {
req: {} as any,
res: {} as any,
}),
).rejects.toThrow()
})
test('case-6: throws', async () => {
const model = new SnippetModel()
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
type: SnippetType.Function,
raw: async function handler(context, require) {
return context.throws(404, 'not found')
}.toString(),
})
expect(
service.injectContextIntoServerlessFunctionAndCall(model, {
req: {} as any,
res: createMockedContextResponse(),
}),
).rejects.toThrow()
})
})
})

View File

@@ -4,12 +4,10 @@ import { getModelForClass } from '@typegoose/typegoose'
import { getModelToken } from 'nestjs-typegoose'
import { dbHelper } from 'test/helper/db-mock.helper'
import { setupE2EApp } from 'test/helper/register-app.helper'
import { ServerlessService } from '~/modules/serverless/serverless.service'
import { SnippetController } from '~/modules/snippet/snippet.controller'
import { SnippetModel, SnippetType } from '~/modules/snippet/snippet.model'
import { SnippetService } from '~/modules/snippet/snippet.service'
import { CacheService } from '~/processors/cache/cache.service'
import { AssetService } from '~/processors/helper/helper.asset.service'
import { HttpService } from '~/processors/helper/helper.http.service'
describe('test /snippets', () => {
let app: NestFastifyApplication
@@ -36,9 +34,15 @@ describe('test /snippets', () => {
controllers: [SnippetController],
providers: [
SnippetService,
AssetService,
HttpService,
{ provide: CacheService, useValue: {} },
{
provide: ServerlessService,
useValue: {
isValidServerlessFunction() {
return true
},
},
},
{
provide: getModelToken(SnippetModel.name),
useValue: model,
@@ -55,7 +59,7 @@ describe('test /snippets', () => {
method: 'POST',
url: '/snippets',
payload: {
name: 'Snippet-1',
name: 'Snippet*1',
private: false,
raw: JSON.stringify({ foo: 'bar' }),
type: SnippetType.JSON,
@@ -124,4 +128,39 @@ describe('test /snippets', () => {
expect(json).toStrictEqual(JSON.parse(mockPayload1.raw))
})
})
const snippetFuncType = {
type: SnippetType.Function,
raw: async function handler(context, require) {
return 1 + 1
}.toString(),
name: 'func-1',
private: false,
reference: 'root',
}
test('POST /snippets, should create function successfully', async () => {
await app
.inject({
method: 'POST',
url: '/snippets',
payload: {
...snippetFuncType,
},
})
.then((res) => {
expect(res.statusCode).toBe(201)
})
})
test('GET /snippets/root/func-1', async () => {
await app
.inject({
method: 'GET',
url: '/snippets/root/func-1',
})
.then((res) => {
expect(res.statusCode).toBe(400)
})
})
})

View File

@@ -3,12 +3,10 @@ import { Test } from '@nestjs/testing'
import { getModelForClass } from '@typegoose/typegoose'
import { getModelToken } from 'nestjs-typegoose'
import { dbHelper } from 'test/helper/db-mock.helper'
import { createMockedContextResponse } from '~/modules/snippet/mock-response.util'
import { ServerlessService } from '~/modules/serverless/serverless.service'
import { SnippetModel, SnippetType } from '~/modules/snippet/snippet.model'
import { SnippetService } from '~/modules/snippet/snippet.service'
import { CacheService } from '~/processors/cache/cache.service'
import { AssetService } from '~/processors/helper/helper.asset.service'
import { HttpService } from '~/processors/helper/helper.http.service'
describe('test Snippet Service', () => {
let service: SnippetService
@@ -18,11 +16,11 @@ describe('test Snippet Service', () => {
const moduleRef = Test.createTestingModule({
providers: [
SnippetService,
AssetService,
HttpService,
{ provide: CacheService, useValue: {} },
{ provide: ServerlessService, useValue: {} },
{
provide: getModelToken('SnippetModel'),
provide: getModelToken(SnippetModel.name),
useValue: getModelForClass(SnippetModel),
},
],
@@ -44,6 +42,7 @@ describe('test Snippet Service', () => {
private: false,
reference: 'root',
}
let id = ''
it('should create one', async () => {
const res = await service.create(snippet)
@@ -68,104 +67,6 @@ describe('test Snippet Service', () => {
expect(res.name).toBe(snippet.name)
})
describe('run serverless function', () => {
test('case-1', async () => {
const model = new SnippetModel()
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
type: SnippetType.Function,
raw: async function handler(context, require) {
return 1 + 1
}.toString(),
})
const data = await service.injectContextIntoServerlessFunctionAndCall(
model,
{ req: {} as any, res: {} as any },
)
expect(data).toBe(2)
})
test('case-2: require built-in module', async () => {
const model = new SnippetModel()
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
type: SnippetType.Function,
raw: async function handler(context, require) {
return (await require('path')).join('1', '1')
}.toString(),
})
const data = await service.injectContextIntoServerlessFunctionAndCall(
model,
{ req: {} as any, res: {} as any },
)
expect(data).toBe('1/1')
})
test('case-3: require extend module', async () => {
const model = new SnippetModel()
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
type: SnippetType.Function,
raw: async function handler(context, require) {
return (await require('axios')).get.toString()
}.toString(),
})
const data = await service.injectContextIntoServerlessFunctionAndCall(
model,
{ req: {} as any, res: {} as any },
)
expect(data).toBeDefined()
})
test('case-4: require ban module', async () => {
const model = new SnippetModel()
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
type: SnippetType.Function,
raw: async function handler(context, require) {
return await require('os')
}.toString(),
})
expect(
service.injectContextIntoServerlessFunctionAndCall(model, {
req: {} as any,
res: {} as any,
}),
).rejects.toThrow()
})
test('case-5: require ban extend module', async () => {
const model = new SnippetModel()
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
type: SnippetType.Function,
raw: async function handler(context, require) {
return await require('@nestjs/core')
}.toString(),
})
expect(
service.injectContextIntoServerlessFunctionAndCall(model, {
req: {} as any,
res: {} as any,
}),
).rejects.toThrow()
})
test('case-6: throws', async () => {
const model = new SnippetModel()
Object.assign<SnippetModel, Partial<SnippetModel>>(model, {
type: SnippetType.Function,
raw: async function handler(context, require) {
return context.throws(404, 'not found')
}.toString(),
})
expect(
service.injectContextIntoServerlessFunctionAndCall(model, {
req: {} as any,
res: createMockedContextResponse(),
}),
).rejects.toThrow()
})
})
test('modify', async () => {
const newSnippet = {
name: 'test',