Added acceptance tests on the v3 api version.

refs https://github.com/TryGhost/Team/issues/221
This commit is contained in:
Thibaut Patel 2021-01-22 17:49:06 +01:00
parent aae58ba4e4
commit 01126cf1bb
5 changed files with 518 additions and 0 deletions

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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}/`);
});
});

View File

@ -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
);
}
};

View File

@ -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;
}
};