From 01126cf1bb6113cb46dc3478fd22cbd34f512bba Mon Sep 17 00:00:00 2001 From: Thibaut Patel Date: Fri, 22 Jan 2021 17:49:06 +0100 Subject: [PATCH] Added acceptance tests on the v3 api version. refs https://github.com/TryGhost/Team/issues/221 --- test/api-acceptance/admin/actions_spec_v3.js | 133 ++++++++++++++ .../admin/key_authentication_spec_v3.js | 71 ++++++++ test/api-acceptance/admin/labels_spec_v3.js | 47 +++++ test/api-acceptance/admin/utils_v3.js | 172 ++++++++++++++++++ test/api-acceptance/content/utils_v3.js | 95 ++++++++++ 5 files changed, 518 insertions(+) create mode 100644 test/api-acceptance/admin/actions_spec_v3.js create mode 100644 test/api-acceptance/admin/key_authentication_spec_v3.js create mode 100644 test/api-acceptance/admin/labels_spec_v3.js create mode 100644 test/api-acceptance/admin/utils_v3.js create mode 100644 test/api-acceptance/content/utils_v3.js diff --git a/test/api-acceptance/admin/actions_spec_v3.js b/test/api-acceptance/admin/actions_spec_v3.js new file mode 100644 index 0000000000..fb2fb351b4 --- /dev/null +++ b/test/api-acceptance/admin/actions_spec_v3.js @@ -0,0 +1,133 @@ +const should = require('should'); +const Promise = require('bluebird'); +const supertest = require('supertest'); +const testUtils = require('../../utils'); +const localUtils = require('./utils'); +const config = require('../../../core/shared/config'); + +describe('Actions API', function () { + let request; + + before(async function () { + await testUtils.startGhost(); + request = supertest.agent(config.get('url')); + await localUtils.doAuth(request, 'integrations', 'api_keys'); + }); + + // @NOTE: This test runs a little slower, because we store Dates without milliseconds. + it('Can request actions for resource', async function () { + let postUpdatedAt; + + const res = await request + .post(localUtils.API.getApiQuery('posts/')) + .set('Origin', config.get('url')) + .send({ + posts: [{ + title: 'test post' + }] + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(201); + + const postId = res.body.posts[0].id; + postUpdatedAt = res.body.posts[0].updated_at; + + const res2 = await request + .get(localUtils.API.getApiQuery(`actions/?filter=resource_id:${postId}&include=actor`)) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200); + + localUtils.API.checkResponse(res2.body, 'actions'); + localUtils.API.checkResponse(res2.body.actions[0], 'action'); + + res2.body.actions.length.should.eql(1); + + res2.body.actions[0].resource_type.should.eql('post'); + res2.body.actions[0].actor_type.should.eql('user'); + res2.body.actions[0].event.should.eql('added'); + Object.keys(res2.body.actions[0].actor).length.should.eql(4); + res2.body.actions[0].actor.id.should.eql(testUtils.DataGenerator.Content.users[0].id); + res2.body.actions[0].actor.image.should.eql(testUtils.DataGenerator.Content.users[0].profile_image); + res2.body.actions[0].actor.name.should.eql(testUtils.DataGenerator.Content.users[0].name); + res2.body.actions[0].actor.slug.should.eql(testUtils.DataGenerator.Content.users[0].slug); + + await Promise.delay(1000); + + const res3 = await request + .put(localUtils.API.getApiQuery(`posts/${postId}/`)) + .set('Origin', config.get('url')) + .send({ + posts: [{ + slug: 'new-slug', + updated_at: postUpdatedAt + }] + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200); + + postUpdatedAt = res3.body.posts[0].updated_at; + + const res4 = await request + .get(localUtils.API.getApiQuery(`actions/?filter=resource_id:${postId}&include=actor`)) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200); + + localUtils.API.checkResponse(res4.body, 'actions'); + localUtils.API.checkResponse(res4.body.actions[0], 'action'); + + res4.body.actions.length.should.eql(2); + + res4.body.actions[0].resource_type.should.eql('post'); + res4.body.actions[0].actor_type.should.eql('user'); + res4.body.actions[0].event.should.eql('edited'); + Object.keys(res4.body.actions[0].actor).length.should.eql(4); + res4.body.actions[0].actor.id.should.eql(testUtils.DataGenerator.Content.users[0].id); + res4.body.actions[0].actor.image.should.eql(testUtils.DataGenerator.Content.users[0].profile_image); + res4.body.actions[0].actor.name.should.eql(testUtils.DataGenerator.Content.users[0].name); + res4.body.actions[0].actor.slug.should.eql(testUtils.DataGenerator.Content.users[0].slug); + + await Promise.delay(1000); + + const integrationRequest = supertest.agent(config.get('url')); + await integrationRequest + .put(localUtils.API.getApiQuery(`posts/${postId}/`)) + .set('Origin', config.get('url')) + .set('Authorization', `Ghost ${localUtils.getValidAdminToken('/v3/admin/')}`) + .send({ + posts: [{ + featured: true, + updated_at: postUpdatedAt + }] + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200); + + const res5 = await request + .get(localUtils.API.getApiQuery(`actions/?filter=resource_id:${postId}&include=actor`)) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200); + + localUtils.API.checkResponse(res5.body, 'actions'); + localUtils.API.checkResponse(res5.body.actions[0], 'action'); + + res5.body.actions.length.should.eql(3); + + res5.body.actions[0].resource_type.should.eql('post'); + res5.body.actions[0].actor_type.should.eql('integration'); + res5.body.actions[0].event.should.eql('edited'); + Object.keys(res5.body.actions[0].actor).length.should.eql(4); + res5.body.actions[0].actor.id.should.eql(testUtils.DataGenerator.Content.integrations[0].id); + should.equal(res5.body.actions[0].actor.image, null); + res5.body.actions[0].actor.name.should.eql(testUtils.DataGenerator.Content.integrations[0].name); + res5.body.actions[0].actor.slug.should.eql(testUtils.DataGenerator.Content.integrations[0].slug); + }); +}); diff --git a/test/api-acceptance/admin/key_authentication_spec_v3.js b/test/api-acceptance/admin/key_authentication_spec_v3.js new file mode 100644 index 0000000000..c5cd8ee6b1 --- /dev/null +++ b/test/api-acceptance/admin/key_authentication_spec_v3.js @@ -0,0 +1,71 @@ +const should = require('should'); +const supertest = require('supertest'); +const testUtils = require('../../utils'); +const config = require('../../../core/shared/config'); +const localUtils = require('./utils'); + +describe('Admin API key authentication', function () { + let request; + + before(async function () { + await testUtils.startGhost(); + request = supertest.agent(config.get('url')); + await testUtils.initFixtures('api_keys'); + }); + + it('Can not access endpoint without a token header', async function () { + await request.get(localUtils.API.getApiQuery('posts/')) + .set('Authorization', `Ghost`) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(401); + }); + + it('Can not access endpoint with a wrong endpoint token', async function () { + await request.get(localUtils.API.getApiQuery('posts/')) + .set('Authorization', `Ghost ${localUtils.getValidAdminToken('https://wrong.com')}`) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(401); + }); + + it('Can access browse endpoint with correct token', async function () { + await request.get(localUtils.API.getApiQuery('posts/')) + .set('Authorization', `Ghost ${localUtils.getValidAdminToken('/v3/admin/')}`) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200); + }); + + it('Can create post', async function () { + const post = { + title: 'Post created with api_key' + }; + + const res = await request + .post(localUtils.API.getApiQuery('posts/?include=authors')) + .set('Origin', config.get('url')) + .set('Authorization', `Ghost ${localUtils.getValidAdminToken('/v3/admin/')}`) + .send({ + posts: [post] + }) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(201); + + // falls back to owner user + res.body.posts[0].authors.length.should.eql(1); + }); + + it('Can read users', async function () { + const res = await request + .get(localUtils.API.getApiQuery('users/')) + .set('Origin', config.get('url')) + .set('Authorization', `Ghost ${localUtils.getValidAdminToken('/v3/admin/')}`) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200); + + localUtils.API.checkResponse(res.body.users[0], 'user'); + }); +}); diff --git a/test/api-acceptance/admin/labels_spec_v3.js b/test/api-acceptance/admin/labels_spec_v3.js new file mode 100644 index 0000000000..52a499c6c5 --- /dev/null +++ b/test/api-acceptance/admin/labels_spec_v3.js @@ -0,0 +1,47 @@ +const path = require('path'); +const should = require('should'); +const supertest = require('supertest'); +const sinon = require('sinon'); +const testUtils = require('../../utils'); +const localUtils = require('../../regression/api/v3/admin/utils'); +const config = require('../../../core/shared/config'); + +describe('Labels API', function () { + let request; + + after(function () { + sinon.restore(); + }); + + before(async function () { + await testUtils.startGhost(); + request = supertest.agent(config.get('url')); + await localUtils.doAuth(request); + }); + + it('Can add', async function () { + const label = { + name: 'test' + }; + + const res = await request + .post(localUtils.API.getApiQuery(`labels/`)) + .send({labels: [label]}) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(201); + + should.not.exist(res.headers['x-cache-invalidate']); + const jsonResponse = res.body; + should.exist(jsonResponse); + should.exist(jsonResponse.labels); + + jsonResponse.labels.should.have.length(1); + jsonResponse.labels[0].name.should.equal(label.name); + jsonResponse.labels[0].slug.should.equal(label.name); + + should.exist(res.headers.location); + res.headers.location.should.equal(`http://127.0.0.1:2369${localUtils.API.getApiQuery('labels/')}${res.body.labels[0].id}/`); + }); +}); diff --git a/test/api-acceptance/admin/utils_v3.js b/test/api-acceptance/admin/utils_v3.js new file mode 100644 index 0000000000..cfd53fa7c6 --- /dev/null +++ b/test/api-acceptance/admin/utils_v3.js @@ -0,0 +1,172 @@ +const url = require('url'); +const _ = require('lodash'); +const testUtils = require('../../utils'); +const schema = require('../../../core/server/data/schema').tables; +const API_URL = '/ghost/api/v3/admin/'; + +const expectedProperties = { + // API top level + posts: ['posts', 'meta'], + pages: ['pages', 'meta'], + tags: ['tags', 'meta'], + users: ['users', 'meta'], + settings: ['settings', 'meta'], + subscribers: ['subscribers', 'meta'], + roles: ['roles'], + pagination: ['page', 'limit', 'pages', 'total', 'next', 'prev'], + slugs: ['slugs'], + slug: ['slug'], + invites: ['invites', 'meta'], + themes: ['themes'], + actions: ['actions', 'meta'], + members: ['members', 'meta'], + snippets: ['snippets', 'meta'], + + action: ['id', 'resource_type', 'actor_type', 'event', 'created_at', 'actor'], + + config: ['version', 'environment', 'database', 'mail', 'labs', 'clientExtensions', 'enableDeveloperExperiments', 'useGravatar', 'stripeDirect', 'emailAnalytics'], + + post: _(schema.posts) + .keys() + // by default we only return mobildoc + .without('html', 'plaintext') + .without('locale') + .without('page') + // v2 API doesn't return new type field + .without('type') + // deprecated + .without('author_id', 'author') + // always returns computed properties + .concat('url', 'primary_tag', 'primary_author', 'excerpt') + // returned by default + .concat('tags', 'authors', 'email') + // returns meta fields from `posts_meta` schema + .concat( + ..._(schema.posts_meta).keys().without('post_id', 'id') + ) + .concat('send_email_when_published') + , + + page: _(schema.posts) + .keys() + // by default we only return mobildoc + .without('html', 'plaintext') + .without('locale') + .without('page') + // v2 API doesn't return new type field + .without('type') + // deprecated + .without('author_id', 'author') + // pages are not sent as emails + .without('email_recipient_filter') + // always returns computed properties + .concat('url', 'primary_tag', 'primary_author', 'excerpt') + // returned by default + .concat('tags', 'authors') + // returns meta fields from `posts_meta` schema + .concat( + ..._(schema.posts_meta).keys() + .without('post_id', 'id') + // pages are not sent as emails + .without('email_subject') + ) + , + + user: _(schema.users) + .keys() + .without('visibility') + .without('password') + .without('locale') + .concat('url') + , + tag: _(schema.tags) + .keys() + // unused field + .without('parent_id') + , + setting: _(schema.settings) + .keys() + , + subscriber: _(schema.subscribers) + .keys() + , + member: _(schema.members) + .keys() + .concat('avatar_image') + .concat('comped') + .concat('labels') + , + member_signin_url: ['member_id', 'url'], + role: _(schema.roles) + .keys() + , + permission: _(schema.permissions) + .keys() + , + notification: ['type', 'message', 'status', 'id', 'dismissible', 'location', 'custom'], + theme: ['name', 'package', 'active'], + invite: _(schema.invites) + .keys() + .without('token') + , + webhook: _(schema.webhooks) + .keys() + , + email: _(schema.emails) + .keys(), + email_preview: ['html', 'subject', 'plaintext'], + email_recipient: _(schema.email_recipients) + .keys() + .filter(key => key.indexOf('@@') === -1), + snippet: _(schema.snippets).keys() +}; + +_.each(expectedProperties, (value, key) => { + if (!value.__wrapped__) { + return; + } + + /** + * @deprecated: x_by + */ + expectedProperties[key] = value + .without( + 'created_by', + 'updated_by', + 'published_by' + ) + .value(); +}); + +module.exports = { + API: { + getApiQuery(route) { + return url.resolve(API_URL, route); + }, + + checkResponse(...args) { + this.expectedProperties = expectedProperties; + return testUtils.API.checkResponse.call(this, ...args); + } + }, + + doAuth(...args) { + return testUtils.API.doAuth(`${API_URL}session/`, ...args); + }, + + getValidAdminToken(audience) { + const jwt = require('jsonwebtoken'); + const JWT_OPTIONS = { + keyid: testUtils.DataGenerator.Content.api_keys[0].id, + algorithm: 'HS256', + expiresIn: '5m', + audience: audience + }; + + return jwt.sign( + {}, + Buffer.from(testUtils.DataGenerator.Content.api_keys[0].secret, 'hex'), + JWT_OPTIONS + ); + } +}; diff --git a/test/api-acceptance/content/utils_v3.js b/test/api-acceptance/content/utils_v3.js new file mode 100644 index 0000000000..b494a2cfca --- /dev/null +++ b/test/api-acceptance/content/utils_v3.js @@ -0,0 +1,95 @@ +const url = require('url'); +const _ = require('lodash'); +const testUtils = require('../../utils'); +const schema = require('../../../core/server/data/schema').tables; +const API_URL = '/ghost/api/v3/content/'; + +const expectedProperties = { + // API top level + posts: ['posts', 'meta'], + tags: ['tags', 'meta'], + authors: ['authors', 'meta'], + pagination: ['page', 'limit', 'pages', 'total', 'next', 'prev'], + + post: _(schema.posts) + .keys() + // by default we only return html + .without('mobiledoc', 'plaintext') + // v2 doesn't return author_id OR author + .without('author_id', 'author') + // and always returns computed properties: url + .concat('url') + // v2 API doesn't return unused fields + .without('locale') + // These fields aren't useful as they always have known values + .without('status') + // v2 API doesn't return new type field + .without('type') + // @TODO: https://github.com/TryGhost/Ghost/issues/10335 + // .without('page') + // v2 returns a calculated excerpt field + .concat('excerpt') + // Access is a calculated property in >= v3 + .concat('access') + // returns meta fields from `posts_meta` schema + .concat( + ..._(schema.posts_meta).keys().without('post_id', 'id') + ) + .concat('reading_time') + .concat('send_email_when_published') + , + author: _(schema.users) + .keys() + .without( + 'password', + 'email', + 'created_at', + 'created_by', + 'updated_at', + 'updated_by', + 'last_seen', + 'status' + ) + // v2 API doesn't return unused fields + .without('accessibility', 'locale', 'tour', 'visibility') + , + tag: _(schema.tags) + .keys() + // v2 Tag API doesn't return parent_id or parent + .without('parent_id', 'parent') + // v2 Tag API doesn't return date fields + .without('created_at', 'updated_at') +}; + +_.each(expectedProperties, (value, key) => { + if (!value.__wrapped__) { + return; + } + + /** + * @deprecated: x_by + */ + expectedProperties[key] = value + .without( + 'created_by', + 'updated_by', + 'published_by' + ) + .value(); +}); + +module.exports = { + API: { + getApiQuery(route) { + return url.resolve(API_URL, route); + }, + + checkResponse(...args) { + this.expectedProperties = expectedProperties; + return testUtils.API.checkResponse.call(this, ...args); + } + }, + getValidKey() { + return testUtils.DataGenerator.Content.api_keys[1].secret; + } +};