mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 11:05:54 +01:00 
			
		
		
		
	feat: wip, write api tests framework
re-using read api tests if possible
This commit is contained in:
		| @@ -34,43 +34,6 @@ function authenticatedRoutes() { | ||||
|  | ||||
| 	setupApiRoute(router, 'post', '/:uid/tokens', [...middlewares, middleware.assert.user, middleware.exposePrivilegeSet], controllers.write.users.generateToken); | ||||
| 	setupApiRoute(router, 'delete', '/:uid/tokens/:token', [...middlewares, middleware.assert.user, middleware.exposePrivilegeSet], controllers.write.users.deleteToken); | ||||
|  | ||||
| 	/** | ||||
| 	 * Implement this later... | ||||
| 	 */ | ||||
| 	// 	app.route('/:uid/tokens') | ||||
| 	// 		.get(apiMiddleware.requireUser, function(req, res) { | ||||
| 	// 			if (parseInt(req.params.uid, 10) !== parseInt(req.user.uid, 10)) { | ||||
| 	// 				return errorHandler.respond(401, res); | ||||
| 	// 			} | ||||
|  | ||||
| 	// 			auth.getTokens(req.params.uid, function(err, tokens) { | ||||
| 	// 				return errorHandler.handle(err, res, { | ||||
| 	// 					tokens: tokens | ||||
| 	// 				}); | ||||
| 	// 			}); | ||||
| 	// 		}) | ||||
| 	// 		.post(apiMiddleware.requireUser, function(req, res) { | ||||
| 	// 			if (parseInt(req.params.uid, 10) !== parseInt(req.user.uid)) { | ||||
| 	// 				return errorHandler.respond(401, res); | ||||
| 	// 			} | ||||
|  | ||||
| 	// 			auth.generateToken(req.params.uid, function(err, token) { | ||||
| 	// 				return errorHandler.handle(err, res, { | ||||
| 	// 					token: token | ||||
| 	// 				}); | ||||
| 	// 			}); | ||||
| 	// 		}); | ||||
|  | ||||
| 	// 	app.delete('/:uid/tokens/:token', apiMiddleware.requireUser, function(req, res) { | ||||
| 	// 		if (parseInt(req.params.uid, 10) !== req.user.uid) { | ||||
| 	// 			return errorHandler.respond(401, res); | ||||
| 	// 		} | ||||
|  | ||||
| 	// 		auth.revokeToken(req.params.token, 'user', function(err) { | ||||
| 	// 			errorHandler.handle(err, res); | ||||
| 	// 		}); | ||||
| 	// 	}); | ||||
| } | ||||
|  | ||||
| module.exports = function () { | ||||
|   | ||||
							
								
								
									
										226
									
								
								test/api.js
									
									
									
									
									
								
							
							
						
						
									
										226
									
								
								test/api.js
									
									
									
									
									
								
							| @@ -21,7 +21,9 @@ const messaging = require('../src/messaging'); | ||||
|  | ||||
| describe('Read API', async () => { | ||||
| 	let readApi = false; | ||||
| 	const apiPath = path.resolve(__dirname, '../public/openapi/read.yaml'); | ||||
| 	let writeApi = false; | ||||
| 	const readApiPath = path.resolve(__dirname, '../public/openapi/read.yaml'); | ||||
| 	const writeApiPath = path.resolve(__dirname, '../public/openapi/write.yaml'); | ||||
| 	let jar; | ||||
| 	let setup = false; | ||||
| 	const unauthenticatedRoutes = ['/api/login', '/api/register'];	// Everything else will be called with the admin user | ||||
| @@ -67,7 +69,7 @@ describe('Read API', async () => { | ||||
| 		await socketUser.exportPosts({ uid: adminUid }, { uid: adminUid }); | ||||
| 		await socketUser.exportUploads({ uid: adminUid }, { uid: adminUid }); | ||||
| 		// wait for export child process to complete | ||||
| 		await wait(20000); | ||||
| 		// await wait(20000); | ||||
|  | ||||
| 		// Attach a search hook so /api/search is enabled | ||||
| 		plugins.registerHook('core', { | ||||
| @@ -81,126 +83,55 @@ describe('Read API', async () => { | ||||
|  | ||||
| 	it('should pass OpenAPI v3 validation', async () => { | ||||
| 		try { | ||||
| 			await SwaggerParser.validate(apiPath); | ||||
| 			await SwaggerParser.validate(readApiPath); | ||||
| 			await SwaggerParser.validate(writeApiPath); | ||||
| 		} catch (e) { | ||||
| 			assert.ifError(e); | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	readApi = await SwaggerParser.dereference(apiPath); | ||||
| 	readApi = await SwaggerParser.dereference(readApiPath); | ||||
| 	writeApi = await SwaggerParser.dereference(writeApiPath); | ||||
|  | ||||
| 	// Iterate through all documented paths, make a call to it, and compare the result body with what is defined in the spec | ||||
| 	const paths = Object.keys(readApi.paths); | ||||
| 	const paths = Object.keys(writeApi.paths); | ||||
| 	// const paths = Object.keys(readApi.paths); | ||||
|  | ||||
| 	paths.forEach((path) => { | ||||
| 		const context = writeApi.paths[path]; | ||||
| 		// const context = readApi.paths[path]; | ||||
| 		let schema; | ||||
| 		let response; | ||||
| 		let url; | ||||
| 		const urls = []; | ||||
| 		const methods = []; | ||||
| 		const headers = {}; | ||||
| 		const qs = {}; | ||||
|  | ||||
| 		function compare(schema, response, context) { | ||||
| 			let required = []; | ||||
| 			const additionalProperties = schema.hasOwnProperty('additionalProperties'); | ||||
|  | ||||
| 			if (schema.allOf) { | ||||
| 				schema = schema.allOf.reduce((memo, obj) => { | ||||
| 					required = required.concat(obj.required ? obj.required : Object.keys(obj.properties)); | ||||
| 					memo = { ...memo, ...obj.properties }; | ||||
| 					return memo; | ||||
| 				}, {}); | ||||
| 			} else if (schema.properties) { | ||||
| 				required = schema.required || Object.keys(schema.properties); | ||||
| 				schema = schema.properties; | ||||
| 			} else { | ||||
| 				// If schema contains no properties, check passes | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			// Compare the schema to the response | ||||
| 			required.forEach((prop) => { | ||||
| 				if (schema.hasOwnProperty(prop)) { | ||||
| 					assert(response.hasOwnProperty(prop), '"' + prop + '" is a required property (path: ' + path + ', context: ' + context + ')'); | ||||
|  | ||||
| 					// Don't proceed with type-check if the value could possibly be unset (nullable: true, in spec) | ||||
| 					if (response[prop] === null && schema[prop].nullable === true) { | ||||
| 						return; | ||||
| 					} | ||||
|  | ||||
| 					// Therefore, if the value is actually null, that's a problem (nullable is probably missing) | ||||
| 					assert(response[prop] !== null, '"' + prop + '" was null, but schema does not specify it to be a nullable property (path: ' + path + ', context: ' + context + ')'); | ||||
|  | ||||
| 					switch (schema[prop].type) { | ||||
| 						case 'string': | ||||
| 							assert.strictEqual(typeof response[prop], 'string', '"' + prop + '" was expected to be a string, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')'); | ||||
| 							break; | ||||
| 						case 'boolean': | ||||
| 							assert.strictEqual(typeof response[prop], 'boolean', '"' + prop + '" was expected to be a boolean, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')'); | ||||
| 							break; | ||||
| 						case 'object': | ||||
| 							assert.strictEqual(typeof response[prop], 'object', '"' + prop + '" was expected to be an object, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')'); | ||||
| 							compare(schema[prop], response[prop], context ? [context, prop].join('.') : prop); | ||||
| 							break; | ||||
| 						case 'array': | ||||
| 							assert.strictEqual(Array.isArray(response[prop]), true, '"' + prop + '" was expected to be an array, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')'); | ||||
|  | ||||
| 							if (schema[prop].items) { | ||||
| 							// Ensure the array items have a schema defined | ||||
| 								assert(schema[prop].items.type || schema[prop].items.allOf, '"' + prop + '" is defined to be an array, but its items have no schema defined (path: ' + path + ', context: ' + context + ')'); | ||||
|  | ||||
| 								// Compare types | ||||
| 								if (schema[prop].items.type === 'object' || Array.isArray(schema[prop].items.allOf)) { | ||||
| 									response[prop].forEach((res) => { | ||||
| 										compare(schema[prop].items, res, context ? [context, prop].join('.') : prop); | ||||
| 									}); | ||||
| 								} else if (response[prop].length) { // for now | ||||
| 									response[prop].forEach((item) => { | ||||
| 										assert.strictEqual(typeof item, schema[prop].items.type, '"' + prop + '" should have ' + schema[prop].items.type + ' items, but found ' + typeof items + ' instead (path: ' + path + ', context: ' + context + ')'); | ||||
| 									}); | ||||
| 								} | ||||
| 							} | ||||
| 							break; | ||||
| 					} | ||||
| 				} | ||||
| 			}); | ||||
|  | ||||
| 			// Compare the response to the schema | ||||
| 			Object.keys(response).forEach((prop) => { | ||||
| 				if (additionalProperties) {	// All bets are off | ||||
| 					return; | ||||
| 				} | ||||
|  | ||||
| 				assert(schema[prop], '"' + prop + '" was found in response, but is not defined in schema (path: ' + path + ', context: ' + context + ')'); | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		// TOXO: fix -- premature exit for POST-only routes | ||||
| 		if (!readApi.paths[path].get) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		it('should have examples when parameters are present', () => { | ||||
| 			const parameters = readApi.paths[path].get.parameters; | ||||
| 			let testPath = path; | ||||
| 			if (parameters) { | ||||
| 				parameters.forEach((param) => { | ||||
| 					assert(param.example !== null && param.example !== undefined, path + ' has parameters without examples'); | ||||
| 			Object.keys(context).forEach((method) => { | ||||
| 				const parameters = context[method].parameters; | ||||
| 				let testPath = path; | ||||
| 				if (parameters) { | ||||
| 					parameters.forEach((param) => { | ||||
| 						assert(param.example !== null && param.example !== undefined, path + ' has parameters without examples'); | ||||
|  | ||||
| 					switch (param.in) { | ||||
| 						case 'path': | ||||
| 							testPath = testPath.replace('{' + param.name + '}', param.example); | ||||
| 							break; | ||||
| 						case 'header': | ||||
| 							headers[param.name] = param.example; | ||||
| 							break; | ||||
| 						case 'query': | ||||
| 							qs[param.name] = param.example; | ||||
| 							break; | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
| 						switch (param.in) { | ||||
| 							case 'path': | ||||
| 								testPath = testPath.replace('{' + param.name + '}', param.example); | ||||
| 								break; | ||||
| 							case 'header': | ||||
| 								headers[param.name] = param.example; | ||||
| 								break; | ||||
| 							case 'query': | ||||
| 								qs[param.name] = param.example; | ||||
| 								break; | ||||
| 						} | ||||
| 					}); | ||||
| 				} | ||||
|  | ||||
| 			url = nconf.get('url') + testPath; | ||||
| 				urls.push = nconf.get('url') + testPath; | ||||
| 				methods.push(method); | ||||
| 			}); | ||||
| 		}); | ||||
|  | ||||
| 		it('should resolve with a 200 when called', async () => { | ||||
| @@ -220,32 +151,93 @@ describe('Read API', async () => { | ||||
|  | ||||
| 		// Recursively iterate through schema properties, comparing type | ||||
| 		it('response should match schema definition', () => { | ||||
| 			const has200 = readApi.paths[path].get.responses['200']; | ||||
| 			const has200 = context.get.responses['200']; | ||||
| 			if (!has200) { | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			const hasJSON = has200.content && has200.content['application/json']; | ||||
| 			if (hasJSON) { | ||||
| 				schema = readApi.paths[path].get.responses['200'].content['application/json'].schema; | ||||
| 				schema = context.get.responses['200'].content['application/json'].schema; | ||||
| 				compare(schema, response, 'root'); | ||||
| 			} | ||||
|  | ||||
| 			// TODO someday: text/csv, binary file type checking? | ||||
| 		}); | ||||
| 	}); | ||||
| }); | ||||
|  | ||||
| describe('Write API', async () => { | ||||
| 	const apiPath = path.resolve(__dirname, '../public/openapi/write.yaml'); | ||||
| 	function compare(schema, response, context) { | ||||
| 		let required = []; | ||||
| 		const additionalProperties = schema.hasOwnProperty('additionalProperties'); | ||||
|  | ||||
| 	it('should pass OpenAPI v3 validation', async () => { | ||||
| 		try { | ||||
| 			await SwaggerParser.validate(apiPath); | ||||
| 		} catch (e) { | ||||
| 			assert.ifError(e); | ||||
| 		if (schema.allOf) { | ||||
| 			schema = schema.allOf.reduce((memo, obj) => { | ||||
| 				required = required.concat(obj.required ? obj.required : Object.keys(obj.properties)); | ||||
| 				memo = { ...memo, ...obj.properties }; | ||||
| 				return memo; | ||||
| 			}, {}); | ||||
| 		} else if (schema.properties) { | ||||
| 			required = schema.required || Object.keys(schema.properties); | ||||
| 			schema = schema.properties; | ||||
| 		} else { | ||||
| 			// If schema contains no properties, check passes | ||||
| 			return; | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	console.log(await SwaggerParser.dereference(apiPath)); | ||||
| 		// Compare the schema to the response | ||||
| 		required.forEach((prop) => { | ||||
| 			if (schema.hasOwnProperty(prop)) { | ||||
| 				assert(response.hasOwnProperty(prop), '"' + prop + '" is a required property (path: ' + path + ', context: ' + context + ')'); | ||||
|  | ||||
| 				// Don't proceed with type-check if the value could possibly be unset (nullable: true, in spec) | ||||
| 				if (response[prop] === null && schema[prop].nullable === true) { | ||||
| 					return; | ||||
| 				} | ||||
|  | ||||
| 				// Therefore, if the value is actually null, that's a problem (nullable is probably missing) | ||||
| 				assert(response[prop] !== null, '"' + prop + '" was null, but schema does not specify it to be a nullable property (path: ' + path + ', context: ' + context + ')'); | ||||
|  | ||||
| 				switch (schema[prop].type) { | ||||
| 					case 'string': | ||||
| 						assert.strictEqual(typeof response[prop], 'string', '"' + prop + '" was expected to be a string, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')'); | ||||
| 						break; | ||||
| 					case 'boolean': | ||||
| 						assert.strictEqual(typeof response[prop], 'boolean', '"' + prop + '" was expected to be a boolean, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')'); | ||||
| 						break; | ||||
| 					case 'object': | ||||
| 						assert.strictEqual(typeof response[prop], 'object', '"' + prop + '" was expected to be an object, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')'); | ||||
| 						compare(schema[prop], response[prop], context ? [context, prop].join('.') : prop); | ||||
| 						break; | ||||
| 					case 'array': | ||||
| 						assert.strictEqual(Array.isArray(response[prop]), true, '"' + prop + '" was expected to be an array, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')'); | ||||
|  | ||||
| 						if (schema[prop].items) { | ||||
| 						// Ensure the array items have a schema defined | ||||
| 							assert(schema[prop].items.type || schema[prop].items.allOf, '"' + prop + '" is defined to be an array, but its items have no schema defined (path: ' + path + ', context: ' + context + ')'); | ||||
|  | ||||
| 							// Compare types | ||||
| 							if (schema[prop].items.type === 'object' || Array.isArray(schema[prop].items.allOf)) { | ||||
| 								response[prop].forEach((res) => { | ||||
| 									compare(schema[prop].items, res, context ? [context, prop].join('.') : prop); | ||||
| 								}); | ||||
| 							} else if (response[prop].length) { // for now | ||||
| 								response[prop].forEach((item) => { | ||||
| 									assert.strictEqual(typeof item, schema[prop].items.type, '"' + prop + '" should have ' + schema[prop].items.type + ' items, but found ' + typeof items + ' instead (path: ' + path + ', context: ' + context + ')'); | ||||
| 								}); | ||||
| 							} | ||||
| 						} | ||||
| 						break; | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		// Compare the response to the schema | ||||
| 		Object.keys(response).forEach((prop) => { | ||||
| 			if (additionalProperties) {	// All bets are off | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			assert(schema[prop], '"' + prop + '" was found in response, but is not defined in schema (path: ' + path + ', context: ' + context + ')'); | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user