sg-openapi-validator
Version:
openapi3.0 schema��� 이용해서 express로 들어오는 req의 값들을 validation 한다.
578 lines (538 loc) • 13.8 kB
JavaScript
const should = require('should')
const utils = require('./utils')
const _ = require('lodash')
suite('spec-validator Test', () => {
suiteSetup(async () => {
this.fullSpec = {
components: {
schemas: {
PhoneNum: {
type: 'string',
pattern: '^+',
minLength: 8,
maxLength: 13,
description: '문자를 받을 수 있는 전화번호, +국가코드 형태가 되어야 한다.',
example: '+821011112222'
},
OfficeNum: {
type: 'string',
minLength: 14,
maxLength: 20,
example: '1234567890123456'
}
},
parameters: {
messageContextOffsetParam: {
in: 'query',
name: 'offset2',
description: '건너띌 row 수, (클라이언트에서 현재까지 로드된 row 수)',
required: false,
schema: {
type: 'integer',
format: 'int32',
default: 0,
minimum: 0
},
example: 0
},
messageContextLimitParam: {
in: 'query',
name: 'limit2',
description: '최대 불러올 데이터 row 수',
required: true,
schema: {
type: 'integer',
format: 'int32',
default: 10,
maximum: 20,
minimum: 1
},
example: 1
}
}
},
paths: {
'/post': {
post: {
requestBody: {
description: '발송 정보 및 수신자 정보와 변환자 정보 객체. authorId의 경우 임의로 아이디 정수형 키 값을 넣어 주어야 한다.',
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: [
'templateKey',
'receivers'
],
properties: {
authorId: {
type: 'integer',
description: '작성자의 아이디, 외부 서비스에서 임의로 넣어주어야 함',
example: 1
},
templateKey: {
type: 'string',
description: '템플릿 키',
example: 'templateKey123',
nullable: false
},
memo: {
type: 'string',
description: '발송자 및 관리자가 볼 메모',
example: '12월 이벤트 문자 발송'
},
receivers: {
type: 'array',
items: {
type: 'object',
required: [
'phoneNum'
],
properties: {
phoneNum: {
$ref: '#/components/schemas/PhoneNum'
},
replacements: {
type: 'array',
description: '변환자 배열, 변환자 키값과, 변환이 되어야할 문자열 객체의 배열.',
items: {
type: 'object',
properties: {
key: {
type: 'string',
description: '해당 템플릿에서 설정한 변환자 키값',
example: 'auth_number'
},
value: {
type: 'string',
description: '해당 변환자의 변환될 값',
example: '123331'
}
}
}
}
}
}
}
}
}
}
}
}
}
},
'/get': {
get: {
parameters: [{
in: 'cookie',
name: 'phoneNum',
schema: {
oneOf: [{
$ref: '#/components/schemas/PhoneNum'
}, {
$ref: '#/components/schemas/OfficeNum'
}]
}
}, {
'$ref': '#/components/parameters/messageContextLimitParam'
}, {
'$ref': '#/components/parameters/messageContextOffsetParam'
}, {
in: 'cookie',
name: 'one',
schema: {
oneOf: [{
type: 'string',
format: 'email'
}, {
type: 'integer'
}]
}
}, {
in: 'header',
name: 'X-API-TOKEN',
description: 'token',
required: true,
schema: {
type: 'string'
},
example: '12ba6s79b9s89c937171ac'
}, {
in: 'query',
name: 'limit',
description: '최대 불러올 데이터 row 수',
required: false,
schema: {
allOf: [{
type: 'integer',
format: 'int32',
default: 10
}, {
type: 'string'
}]
}
}, {
in: 'query',
name: 'limit',
description: '최대 불러올 데이터 row 수',
required: false,
schema: {
type: 'integer',
format: 'int32',
default: 10,
maximum: 10,
minimum: 1
},
example: 10
}, {
in: 'query',
name: 'key',
description: '템플릿의 키 겁색 (prefix 검색 가능)',
required: true,
schema: {
type: 'string',
example: 'templateKey123'
}
}, {
in: 'query',
name: 'title',
description: '템플릿의 제목 겁색 (prefix 검색 가능)',
required: false,
schema: {
type: 'string',
example: '인증번호',
minLength: 1,
maxLength: 10
}
}, {
in: 'query',
name: 'sortKey',
description: '템플릿 키로 정렬한다.',
required: false,
schema: {
type: 'string',
enum: ['desc', 'asc'],
default: 'desc',
example: 'desc'
}
}, {
in: 'path',
name: 'id',
description: '템플릿의 아이디',
required: true,
schema: {
type: 'integer',
example: 1
}
}, {
in: 'path',
name: 'test',
required: true,
schema: {
type: 'array',
items: {
type: 'object',
properties: {
id: {
type: 'integer',
minimum: 1,
maximum: 2
},
data: {
type: 'object',
required: [
'key',
'key2'
],
properties: {
key: {
type: 'string',
nullable: false,
minLength: 1,
maxLength: 1
},
key2: {
type: 'string',
maxLength: 1,
nullable: true
}
}
}
}
}
}
}]
}
}
}
}
})
suite('replaceExpressKeywordToOpenAPIKeywor method test', () => {
test('should success when url is valid', () => {
let url = '/test/:test/:test2/aaaaa/test'
url = utils.replaceExpressKeywordToOpenAPIKeyword(url)
url.should.eql('/test/{test}/{test2}/aaaaa/test')
})
test('should success when other url is valid', () => {
let url = '/test/:test/:test2/aaaaa/:test'
url = utils.replaceExpressKeywordToOpenAPIKeyword(url)
url.should.eql('/test/{test}/{test2}/aaaaa/{test}')
})
})
suite('replaceRealKeywordToOpenAPIKeyword method test', () => {
test('should success when url is valid', () => {
let paths = {
'/test/{test}/{test2}/aaaaa/test': {},
'/test/{test}/{test2}/aaaaa/test/{app}': {}
}
let url = '/test/1/2/aaaaa/test'
url = utils.replaceRealKeywordToOpenAPIKeyword(url, paths)
url.should.eql('/test/{test}/{test2}/aaaaa/test')
})
})
suite('validateRequestBody method test', async () => {
test('should success to validate from body and return empty array.', async () => {
let body = {
authorId: 1,
templateKey: 'key',
memo: 'memo',
receivers: [{
phoneNum: '81899191111',
replacements: [{
key: 'key',
value: 'val'
}, {
key: 'key2',
value: 'val2'
}]
}, {
phoneNum: '81899191112',
replacements: [{
key: 'key11',
value: 'val11'
}, {
key: 'key211',
value: 'val211'
}]
}]
}
let errors = utils.validateRequestBody(this.fullSpec, this.fullSpec.paths['/post'].post.requestBody, body)
errors.length.should.eql(0)
})
test('should fail to validate from body and return errors.', async () => {
let body = {
authorId: 'a',
memo: '',
receivers: [{
replacements: [{
key: 'key',
value: 'val'
}, {
key: 'key2',
value: 'val2'
}]
}, {
phoneNum: 'ㅁㅁ',
replacements: [{
key: 'key11',
value: 'val11'
}, {
key: 'key211',
value: 'val211'
}]
}]
}
let errors = utils.validateRequestBody(this.fullSpec, this.fullSpec.paths['/post'].post.requestBody, body)
let obj = _.keyBy(errors, 'param')
obj.authorId.should.property('code', utils.codes.invalidInteger)
obj.templateKey.should.property('code', utils.codes.required)
obj['receivers[0].phoneNum'].should.property('code', utils.codes.required)
obj['receivers[1].phoneNum'].should.property('code', utils.codes.invalidString)
})
})
suite('validateParameters method test', async () => {
test('should success to validate and return empty array.', async () => {
let query = {
limit: 10,
title: 'title',
key: 'key',
sortTitle: 'desc',
sortKey: 'desc',
offset2: 1,
limit2: 2
}
let params = {
id: 1,
test: [{
id: 1,
data: {
key: 'a',
key2: null
}
}]
}
let header = {
'X-API-TOKEN': '123123123123'
}
let cookie = {
one: 'aa@anve.com',
phoneNum: '+821011112222'
}
let errors = utils.validateParameters(this.fullSpec, this.fullSpec.paths['/get'].get.parameters, query, params, header, cookie)
errors.length.should.eql(0)
})
test('should fail to success all parameters validation and return error array.', async () => {
let query = {
limit: 100,
title: '123456789012345',
sortTitle: 'desc2',
sortKey: 'desc2'
}
let params = {
id: 'a',
test: [{
id: 10,
data: {
key: null
}
}, {
id: 100000000,
data: {
key: null,
key2: 'aa'
}
}]
}
let cookie = {
one: 'a',
phoneNum: '1'
}
let errors = utils.validateParameters(this.fullSpec, this.fullSpec.paths['/get'].get.parameters, query, params, {}, cookie)
_.map(errors, (error) => {
error.should.have.properties(['domain', 'code', 'msg', 'param', 'value'])
})
let obj = _.keyBy(errors, 'param')
obj.limit.should.have.property('code', utils.codes.invalidInteger)
obj.limit.should.have.property('domain', 'query')
obj.title.should.have.property('code', utils.codes.invalidString)
obj.title.should.have.property('domain', 'query')
obj.sortKey.should.have.property('code', utils.codes.invalidEnum)
obj.sortKey.should.have.property('domain', 'query')
obj.id.should.have.property('code', utils.codes.invalidInteger)
obj.id.should.have.property('domain', 'path')
obj['test[0].id'].should.have.property('code', utils.codes.invalidInteger)
obj['test[0].data.key'].should.have.property('code', utils.codes.prohibitNull)
obj['test[0].data.key2'].should.have.property('code', utils.codes.required)
obj['test[1].id'].should.have.property('code', utils.codes.invalidInteger)
obj['test[1].data.key'].should.have.property('code', utils.codes.prohibitNull)
obj['test[1].data.key2'].should.have.property('code', utils.codes.invalidString)
obj['X-API-TOKEN'].should.have.property('code', utils.codes.prohibitNull)
obj.limit2.should.have.property('code', utils.codes.prohibitNull)
obj.one.should.have.property('code', utils.codes.invalidOf)
obj.one.should.have.property('domain', 'cookie')
obj.phoneNum.should.have.property('code', utils.codes.invalidOf)
})
test('convertParameterType method test', () => {
const parameters = [{
in: 'query',
name: 'name',
description: 'user name',
required: true,
schema: {
type: 'string',
example: 'hwarang',
nullable: false
}
}, {
in: 'query',
name: 'location',
description: 'user location',
required: true,
schema: {
type: ['array', 'null'],
items: {
type: ['integer', 'string', 'null']
}
}
}, {
in: 'query',
name: 'test',
description: 'user test',
required: true,
schema: {
type: ['array', 'null'],
items: {
type: ['string', 'null']
}
}
}, {
in: 'path',
name: 'age',
description: 'user integer age',
required: true,
schema: {
type: 'integer',
example: 22,
nullable: false
}
}, {
in: 'path',
name: 'readable',
description: 'user readable flag',
required: true,
schema: {
type: 'boolean',
example: 22,
nullable: false
}
}, {
in: 'path',
name: 'real_location',
description: 'user test2',
required: true,
schema: {
type: ['object', 'null'],
properties: {
lat: {
type: 'number'
},
lng: {
type: 'number'
}
}
}
}]
let query = {
'name': 'hwarang',
'location': ['test', '11', ''],
'test': ['test', '']
}
let params = {
age: '11',
readable: 'true',
real_location: {
lat: '100.001',
lng: '200.22'
}
}
let req = {
query,
params
}
req = utils.convertParameterType(parameters, req)
req.query.name.should.String()
req.query.location[0].should.String()
req.query.location[1].should.Number()
should.not.exists(req.query.location[2])
req.query.test[0].should.String()
should.not.exists(req.query.test[1])
req.params.age.should.Number()
req.params.readable.should.Boolean()
req.params.real_location.lat.should.Number()
req.params.real_location.lng.should.Number()
})
})
suiteTeardown(() => {
})
})