mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 11:05:54 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			244 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			244 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| const assert = require('assert');
 | |
| const path = require('path');
 | |
| const SwaggerParser = require('@apidevtools/swagger-parser');
 | |
| const request = require('request-promise-native');
 | |
| const nconf = require('nconf');
 | |
| const util = require('util');
 | |
| const wait = util.promisify(setTimeout);
 | |
| 
 | |
| const db = require('./mocks/databasemock');
 | |
| const helpers = require('./helpers');
 | |
| const user = require('../src/user');
 | |
| const groups = require('../src/groups');
 | |
| const categories = require('../src/categories');
 | |
| const topics = require('../src/topics');
 | |
| const plugins = require('../src/plugins');
 | |
| const flags = require('../src/flags');
 | |
| const messaging = require('../src/messaging');
 | |
| 
 | |
| 
 | |
| describe('Read API', async () => {
 | |
| 	let readApi = false;
 | |
| 	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
 | |
| 
 | |
| 	async function dummySearchHook(data) {
 | |
| 		return [1];
 | |
| 	}
 | |
| 
 | |
| 	after(async function () {
 | |
| 		plugins.unregisterHook('core', 'filter:search.query', dummySearchHook);
 | |
| 	});
 | |
| 
 | |
| 	async function setupData() {
 | |
| 		if (setup) {
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		// Create sample users
 | |
| 		const adminUid = await user.create({ username: 'admin', password: '123456', email: 'test@example.org' });
 | |
| 		const unprivUid = await user.create({ username: 'unpriv', password: '123456', email: 'unpriv@example.org' });
 | |
| 		await groups.join('administrators', adminUid);
 | |
| 
 | |
| 		// Create a category
 | |
| 		const testCategory = await categories.create({ name: 'test' });
 | |
| 
 | |
| 		// Post a new topic
 | |
| 		const testTopic = await topics.post({
 | |
| 			uid: adminUid,
 | |
| 			cid: testCategory.cid,
 | |
| 			title: 'Test Topic',
 | |
| 			content: 'Test topic content',
 | |
| 		});
 | |
| 
 | |
| 		// Create a sample flag
 | |
| 		await flags.create('post', 1, unprivUid, 'sample reasons', Date.now());
 | |
| 
 | |
| 		// Create a new chat room
 | |
| 		await messaging.newRoom(1, [2]);
 | |
| 
 | |
| 		const socketUser = require('../src/socket.io/user');
 | |
| 		// export data for admin user
 | |
| 		await socketUser.exportProfile({ uid: adminUid }, { uid: adminUid });
 | |
| 		await socketUser.exportPosts({ uid: adminUid }, { uid: adminUid });
 | |
| 		await socketUser.exportUploads({ uid: adminUid }, { uid: adminUid });
 | |
| 		// wait for export child process to complete
 | |
| 		// await wait(20000);
 | |
| 
 | |
| 		// Attach a search hook so /api/search is enabled
 | |
| 		plugins.registerHook('core', {
 | |
| 			hook: 'filter:search.query',
 | |
| 			method: dummySearchHook,
 | |
| 		});
 | |
| 
 | |
| 		jar = await helpers.loginUser('admin', '123456');
 | |
| 		setup = true;
 | |
| 	}
 | |
| 
 | |
| 	it('should pass OpenAPI v3 validation', async () => {
 | |
| 		try {
 | |
| 			await SwaggerParser.validate(readApiPath);
 | |
| 			await SwaggerParser.validate(writeApiPath);
 | |
| 		} catch (e) {
 | |
| 			assert.ifError(e);
 | |
| 		}
 | |
| 	});
 | |
| 
 | |
| 	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(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;
 | |
| 		const urls = [];
 | |
| 		const methods = [];
 | |
| 		const headers = {};
 | |
| 		const qs = {};
 | |
| 
 | |
| 		it('should have examples when parameters are present', () => {
 | |
| 			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;
 | |
| 						}
 | |
| 					});
 | |
| 				}
 | |
| 
 | |
| 				urls.push = nconf.get('url') + testPath;
 | |
| 				methods.push(method);
 | |
| 			});
 | |
| 		});
 | |
| 
 | |
| 		it('should resolve with a 200 when called', async () => {
 | |
| 			await setupData();
 | |
| 
 | |
| 			try {
 | |
| 				response = await request(url, {
 | |
| 					jar: !unauthenticatedRoutes.includes(path) ? jar : undefined,
 | |
| 					json: true,
 | |
| 					headers: headers,
 | |
| 					qs: qs,
 | |
| 				});
 | |
| 			} catch (e) {
 | |
| 				assert(!e, path + ' resolved with ' + e.message);
 | |
| 			}
 | |
| 		});
 | |
| 
 | |
| 		// Recursively iterate through schema properties, comparing type
 | |
| 		it('response should match schema definition', () => {
 | |
| 			const has200 = context.get.responses['200'];
 | |
| 			if (!has200) {
 | |
| 				return;
 | |
| 			}
 | |
| 
 | |
| 			const hasJSON = has200.content && has200.content['application/json'];
 | |
| 			if (hasJSON) {
 | |
| 				schema = context.get.responses['200'].content['application/json'].schema;
 | |
| 				compare(schema, response, 'root');
 | |
| 			}
 | |
| 
 | |
| 			// TODO someday: text/csv, binary file type checking?
 | |
| 		});
 | |
| 	});
 | |
| 
 | |
| 	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 + ')');
 | |
| 		});
 | |
| 	}
 | |
| });
 |