UNPKG

sg-openapi-validator

Version:

openapi3.0 schema��� 이용해서 express로 들어오는 req의 값들을 validation 한다.

578 lines (538 loc) 13.8 kB
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(() => { }) })