From 595068331672a5eea8dd57466e3f918586a7a28a Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 4 Dec 2020 12:56:55 -0500 Subject: [PATCH] feat: closes #9048, tests for topic thumbs routes, write API schema --- public/openapi/write.yaml | 2 + public/openapi/write/topics/tid/thumbs.yaml | 146 ++++++++++++++++++++ src/controllers/uploads.js | 2 +- test/api.js | 54 ++++++-- 4 files changed, 188 insertions(+), 16 deletions(-) create mode 100644 public/openapi/write/topics/tid/thumbs.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 1739a58880..73555fe7ac 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -76,6 +76,8 @@ paths: $ref: 'write/topics/tid/ignore.yaml' /topics/{tid}/tags: $ref: 'write/topics/tid/tags.yaml' + /topics/{tid}/thumbs: + $ref: 'write/topics/tid/thumbs.yaml' /posts/{pid}: $ref: 'write/posts/pid.yaml' /posts/{pid}/state: diff --git a/public/openapi/write/topics/tid/thumbs.yaml b/public/openapi/write/topics/tid/thumbs.yaml new file mode 100644 index 0000000000..d8c914ec01 --- /dev/null +++ b/public/openapi/write/topics/tid/thumbs.yaml @@ -0,0 +1,146 @@ +get: + tags: + - topics + summary: get topic thumbnails + description: This operation retrieves a topic's uploaded thumbnails + parameters: + - in: path + name: tid + schema: + type: string + required: true + description: a valid topic id + example: 1 + responses: + '200': + description: Thumbnails successfully retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: {} +post: + tags: + - topics + summary: add topic thumbnail + description: This operation adds a thumbnail to an existing topic or a draft (via a composer `uuid`) + parameters: + - in: path + name: tid + schema: + type: string + required: true + description: a valid topic id + example: 1 + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + files: + type: array + items: + type: string + format: binary + responses: + '200': + description: Thumbnail successfully added + content: + application/json: + schema: + type: array + items: + type: object + properties: + url: + type: string + path: + type: string + name: + type: string +put: + tags: + - topics + summary: migrate topic thumbnail + description: This operation migrates a thumbnails from a topic or draft, to another tid or draft. + parameters: + - in: path + name: tid + schema: + type: string + required: true + description: a valid topic id or draft uuid + example: 1 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + tid: + type: string + description: a valid topic id or draft uuid + example: '1' + responses: + '200': + description: Topic thumbnails migrated + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: {} +delete: + tags: + - topics + summary: remove topic thumbnail + description: This operation removes a topic thumbnail. + parameters: + - in: path + name: tid + schema: + type: string + required: true + description: a valid topic id + example: 1 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + path: + type: string + description: Relative path to the topic thumbnail + example: files/test.png + responses: + '200': + description: Topic thumbnail removed + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: array + description: A list of the topic thumbnails that still remain + items: + type: object + properties: + url: + type: string + description: Path to a topic thumbnail \ No newline at end of file diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js index dc6e744807..e4dd6f73e3 100644 --- a/src/controllers/uploads.js +++ b/src/controllers/uploads.js @@ -16,10 +16,10 @@ const uploadsController = module.exports; uploadsController.upload = async function (req, res, filesIterator) { let files = req.files.files; + // These checks added because of odd behaviour by request: https://github.com/request/request/issues/2445 if (!Array.isArray(files)) { return res.status(500).json('invalid files'); } - if (Array.isArray(files[0])) { files = files[0]; } diff --git a/test/api.js b/test/api.js index 535e7ea19e..1f43e67e13 100644 --- a/test/api.js +++ b/test/api.js @@ -99,6 +99,7 @@ describe('API', async () => { timestamp: Date.now(), }], }); + meta.config.allowTopicsThumbnail = 1; // Create a category const testCategory = await categories.create({ name: 'test' }); @@ -123,8 +124,9 @@ describe('API', async () => { // Create a new chat room await messaging.newRoom(1, [2]); - // Create an empty file to test DELETE /files + // Create an empty file to test DELETE /files and thumb deletion fs.closeSync(fs.openSync(path.resolve(nconf.get('upload_path'), 'files/test.txt'), 'w')); + fs.closeSync(fs.openSync(path.resolve(nconf.get('upload_path'), 'files/test.png'), 'w')); const socketUser = require('../src/socket.io/user'); const socketAdmin = require('../src/socket.io/admin'); @@ -172,6 +174,7 @@ describe('API', async () => { function generateTests(api, paths, prefix) { // Iterate through all documented paths, make a call to it, and compare the result body with what is defined in the spec + const pathLib = path; // for calling path module from inside this forEach paths.forEach((path) => { const context = api.paths[path]; let schema; @@ -224,13 +227,20 @@ describe('API', async () => { url = nconf.get('url') + (prefix || '') + testPath; }); - it('should contain a valid request body (if present) with application/json type if POST/PUT/DELETE', () => { + it('should contain a valid request body (if present) with application/json or multipart/form-data type if POST/PUT/DELETE', () => { if (['post', 'put', 'delete'].includes(method) && context[method].hasOwnProperty('requestBody')) { assert(context[method].requestBody); assert(context[method].requestBody.content); - assert(context[method].requestBody.content['application/json']); - assert(context[method].requestBody.content['application/json'].schema); - assert(context[method].requestBody.content['application/json'].schema.properties); + + if (context[method].requestBody.content.hasOwnProperty('application/json')) { + assert(context[method].requestBody.content['application/json']); + assert(context[method].requestBody.content['application/json'].schema); + assert(context[method].requestBody.content['application/json'].schema.properties); + } else if (context[method].requestBody.content.hasOwnProperty('multipart/form-data')) { + assert(context[method].requestBody.content['multipart/form-data']); + assert(context[method].requestBody.content['multipart/form-data'].schema); + assert(context[method].requestBody.content['multipart/form-data'].schema.properties); + } } }); @@ -242,20 +252,34 @@ describe('API', async () => { } let body = {}; - if (context[method].hasOwnProperty('requestBody')) { + let type = 'json'; + if (context[method].hasOwnProperty('requestBody') && context[method].requestBody.content['application/json']) { body = buildBody(context[method].requestBody.content['application/json'].schema.properties); + } else if (context[method].hasOwnProperty('requestBody') && context[method].requestBody.content['multipart/form-data']) { + type = 'form'; } try { - // console.log(`calling ${method} ${url} with`, body); - response = await request(url, { - method: method, - jar: !unauthenticatedRoutes.includes(path) ? jar : undefined, - json: true, - headers: headers, - qs: qs, - body: body, - }); + if (type === 'json') { + // console.log(`calling ${method} ${url} with`, body); + response = await request(url, { + method: method, + jar: !unauthenticatedRoutes.includes(path) ? jar : undefined, + json: true, + headers: headers, + qs: qs, + body: body, + }); + } else if (type === 'form') { + response = await new Promise((resolve, reject) => { + helpers.uploadFile(url, pathLib.join(__dirname, './files/test.png'), {}, jar, csrfToken, function (err, res, body) { + if (err) { + return reject(err); + } + resolve(body); + }); + }); + } } catch (e) { assert(!e, `${method.toUpperCase()} ${path} resolved with ${e.message}`); }