Added audience feedback service and storage (#15584)
fixes https://github.com/TryGhost/Team/issues/2049 fixes https://github.com/TryGhost/Team/issues/2053 - This adds a new audience feedback package to Ghost. - A new members API to give feedback on posts using the `/api/feedback` endpoint. - Added a new authentication middleware that supports both uuid-based and session based authentication.
This commit is contained in:
parent
6ff34fb49f
commit
e540344ef2
6
ghost/audience-feedback/.eslintrc.js
Normal file
6
ghost/audience-feedback/.eslintrc.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: ['ghost'],
|
||||||
|
extends: [
|
||||||
|
'plugin:ghost/node'
|
||||||
|
]
|
||||||
|
};
|
21
ghost/audience-feedback/README.md
Normal file
21
ghost/audience-feedback/README.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Audience Feedback
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
|
||||||
|
## Develop
|
||||||
|
|
||||||
|
This is a monorepo package.
|
||||||
|
|
||||||
|
Follow the instructions for the top-level repo.
|
||||||
|
1. `git clone` this repo & `cd` into it as usual
|
||||||
|
2. Run `yarn` to install top-level dependencies.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
- `yarn lint` run just eslint
|
||||||
|
- `yarn test` run lint and tests
|
||||||
|
|
1
ghost/audience-feedback/index.js
Normal file
1
ghost/audience-feedback/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./lib/audience-feedback');
|
85
ghost/audience-feedback/lib/AudienceFeedbackController.js
Normal file
85
ghost/audience-feedback/lib/AudienceFeedbackController.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
const Feedback = require('./Feedback');
|
||||||
|
const errors = require('@tryghost/errors');
|
||||||
|
const tpl = require('@tryghost/tpl');
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
invalidScore: 'Invalid feedback score. Only 1 or 0 is currently allowed.',
|
||||||
|
postNotFound: 'Post not found.',
|
||||||
|
memberNotFound: 'Member not found.'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} IFeedbackRepository
|
||||||
|
* @prop {(feedback: Feedback) => Promise<void>} add
|
||||||
|
* @prop {(feedback: Feedback) => Promise<void>} edit
|
||||||
|
* @prop {(postId, memberId) => Promise<Feedback>} get
|
||||||
|
* @prop {(id: string) => Promise<Post|undefined>} getPostById
|
||||||
|
*/
|
||||||
|
|
||||||
|
class AudienceFeedbackController {
|
||||||
|
/** @type IFeedbackRepository */
|
||||||
|
#repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} deps
|
||||||
|
* @param {IFeedbackRepository} deps.repository
|
||||||
|
*/
|
||||||
|
constructor(deps) {
|
||||||
|
this.#repository = deps.repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get member from frame
|
||||||
|
*/
|
||||||
|
#getMember(frame) {
|
||||||
|
if (!frame.options?.context?.member?.id) {
|
||||||
|
// This is an internal server error because authentication should happen outside this service.
|
||||||
|
throw new errors.InternalServerError({
|
||||||
|
message: tpl(messages.memberNotFound)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return frame.options.context.member;
|
||||||
|
}
|
||||||
|
|
||||||
|
async add(frame) {
|
||||||
|
const data = frame.data.feedback[0];
|
||||||
|
const postId = data.post_id;
|
||||||
|
const score = data.score;
|
||||||
|
|
||||||
|
if (![0, 1].includes(score)) {
|
||||||
|
throw new errors.ValidationError({
|
||||||
|
message: tpl(messages.invalidScore)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = this.#getMember(frame);
|
||||||
|
|
||||||
|
const post = await this.#repository.getPostById(postId);
|
||||||
|
if (!post) {
|
||||||
|
throw new errors.NotFoundError({
|
||||||
|
message: tpl(messages.postNotFound)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await this.#repository.get(post.id, member.id);
|
||||||
|
if (existing) {
|
||||||
|
if (existing.score === score) {
|
||||||
|
// Don't save so we don't update the updated_at timestamp
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
existing.score = score;
|
||||||
|
await this.#repository.edit(existing);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedback = new Feedback({
|
||||||
|
memberId: member.id,
|
||||||
|
postId: post.id,
|
||||||
|
score
|
||||||
|
});
|
||||||
|
await this.#repository.add(feedback);
|
||||||
|
return feedback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AudienceFeedbackController;
|
8
ghost/audience-feedback/lib/AudienceFeedbackService.js
Normal file
8
ghost/audience-feedback/lib/AudienceFeedbackService.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
class AudienceFeedbackService {
|
||||||
|
buildLink() {
|
||||||
|
// todo
|
||||||
|
return new URL('https://example.com');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AudienceFeedbackService;
|
35
ghost/audience-feedback/lib/Feedback.js
Normal file
35
ghost/audience-feedback/lib/Feedback.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
const ObjectID = require('bson-objectid').default;
|
||||||
|
|
||||||
|
module.exports = class Feedback {
|
||||||
|
/** @type {ObjectID} */
|
||||||
|
id;
|
||||||
|
/** @type {number} */
|
||||||
|
score;
|
||||||
|
/** @type {ObjectID} */
|
||||||
|
memberId;
|
||||||
|
/** @type {ObjectID} */
|
||||||
|
postId;
|
||||||
|
|
||||||
|
constructor(data) {
|
||||||
|
if (!data.id) {
|
||||||
|
this.id = new ObjectID();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data.id === 'string') {
|
||||||
|
this.id = ObjectID.createFromHexString(data.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.score = data.score ?? 0;
|
||||||
|
if (typeof data.memberId === 'string') {
|
||||||
|
this.memberId = ObjectID.createFromHexString(data.memberId);
|
||||||
|
} else {
|
||||||
|
this.memberId = data.memberId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data.postId === 'string') {
|
||||||
|
this.postId = ObjectID.createFromHexString(data.postId);
|
||||||
|
} else {
|
||||||
|
this.postId = data.postId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
5
ghost/audience-feedback/lib/audience-feedback.js
Normal file
5
ghost/audience-feedback/lib/audience-feedback.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
AudienceFeedbackService: require('./AudienceFeedbackService'),
|
||||||
|
AudienceFeedbackController: require('./AudienceFeedbackController'),
|
||||||
|
Feedback: require('./Feedback')
|
||||||
|
};
|
29
ghost/audience-feedback/package.json
Normal file
29
ghost/audience-feedback/package.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "@tryghost/audience-feedback",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"repository": "https://github.com/TryGhost/Ghost/tree/main/packages/audience-feedback",
|
||||||
|
"author": "Ghost Foundation",
|
||||||
|
"private": true,
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "echo \"Implement me!\"",
|
||||||
|
"test:unit": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'",
|
||||||
|
"test": "yarn test:unit",
|
||||||
|
"lint:code": "eslint *.js lib/ --ext .js --cache",
|
||||||
|
"lint": "yarn lint:code && yarn lint:test",
|
||||||
|
"lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"index.js",
|
||||||
|
"lib"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"c8": "7.12.0",
|
||||||
|
"mocha": "10.0.0",
|
||||||
|
"should": "13.2.3",
|
||||||
|
"sinon": "14.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tryghost/errors": "1.2.17"
|
||||||
|
}
|
||||||
|
}
|
6
ghost/audience-feedback/test/.eslintrc.js
Normal file
6
ghost/audience-feedback/test/.eslintrc.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: ['ghost'],
|
||||||
|
extends: [
|
||||||
|
'plugin:ghost/test'
|
||||||
|
]
|
||||||
|
};
|
10
ghost/audience-feedback/test/hello.test.js
Normal file
10
ghost/audience-feedback/test/hello.test.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// Switch these lines once there are useful utils
|
||||||
|
// const testUtils = require('./utils');
|
||||||
|
require('./utils');
|
||||||
|
|
||||||
|
describe('Hello world', function () {
|
||||||
|
it('Runs a test', function () {
|
||||||
|
// TODO: Write me!
|
||||||
|
'hello'.should.eql('hello');
|
||||||
|
});
|
||||||
|
});
|
11
ghost/audience-feedback/test/utils/assertions.js
Normal file
11
ghost/audience-feedback/test/utils/assertions.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Custom Should Assertions
|
||||||
|
*
|
||||||
|
* Add any custom assertions to this file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Example Assertion
|
||||||
|
// should.Assertion.add('ExampleAssertion', function () {
|
||||||
|
// this.params = {operator: 'to be a valid Example Assertion'};
|
||||||
|
// this.obj.should.be.an.Object;
|
||||||
|
// });
|
11
ghost/audience-feedback/test/utils/index.js
Normal file
11
ghost/audience-feedback/test/utils/index.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Test Utilities
|
||||||
|
*
|
||||||
|
* Shared utils for writing tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Require overrides - these add globals for tests
|
||||||
|
require('./overrides');
|
||||||
|
|
||||||
|
// Require assertions - adds custom should assertions
|
||||||
|
require('./assertions');
|
10
ghost/audience-feedback/test/utils/overrides.js
Normal file
10
ghost/audience-feedback/test/utils/overrides.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// This file is required before any test is run
|
||||||
|
|
||||||
|
// Taken from the should wiki, this is how to make should global
|
||||||
|
// Should is a global in our eslint test config
|
||||||
|
global.should = require('should').noConflict();
|
||||||
|
should.extend();
|
||||||
|
|
||||||
|
// Sinon is a simple case
|
||||||
|
// Sinon is a global in our eslint test config
|
||||||
|
global.sinon = require('sinon');
|
@ -288,6 +288,7 @@ async function initServices({config}) {
|
|||||||
const memberAttribution = require('./server/services/member-attribution');
|
const memberAttribution = require('./server/services/member-attribution');
|
||||||
const membersEvents = require('./server/services/members-events');
|
const membersEvents = require('./server/services/members-events');
|
||||||
const linkTracking = require('./server/services/link-tracking');
|
const linkTracking = require('./server/services/link-tracking');
|
||||||
|
const audienceFeedback = require('./server/services/audience-feedback');
|
||||||
|
|
||||||
const urlUtils = require('./shared/url-utils');
|
const urlUtils = require('./shared/url-utils');
|
||||||
|
|
||||||
@ -315,7 +316,8 @@ async function initServices({config}) {
|
|||||||
apiUrl: urlUtils.urlFor('api', {type: 'admin'}, true)
|
apiUrl: urlUtils.urlFor('api', {type: 'admin'}, true)
|
||||||
}),
|
}),
|
||||||
comments.init(),
|
comments.init(),
|
||||||
linkTracking.init()
|
linkTracking.init(),
|
||||||
|
audienceFeedback.init()
|
||||||
]);
|
]);
|
||||||
debug('End: Services');
|
debug('End: Services');
|
||||||
|
|
||||||
|
23
ghost/core/core/server/api/endpoints/feedback-members.js
Normal file
23
ghost/core/core/server/api/endpoints/feedback-members.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const feedbackService = require('../../services/audience-feedback');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
docName: 'feedback',
|
||||||
|
|
||||||
|
add: {
|
||||||
|
statusCode: 201,
|
||||||
|
validation: {
|
||||||
|
data: {
|
||||||
|
post_id: {
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
score: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
permissions: false,
|
||||||
|
query(frame) {
|
||||||
|
return feedbackService.controller.add(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -231,5 +231,9 @@ module.exports = {
|
|||||||
|
|
||||||
get commentsMembers() {
|
get commentsMembers() {
|
||||||
return apiFramework.pipeline(require('./comments-members'), localUtils, 'members');
|
return apiFramework.pipeline(require('./comments-members'), localUtils, 'members');
|
||||||
}
|
},
|
||||||
|
|
||||||
|
get feedbackMembers() {
|
||||||
|
return apiFramework.pipeline(require('./feedback-members'), localUtils, 'members');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
module.exports = class FeedbackRepository {
|
||||||
|
/** @type {object} */
|
||||||
|
#Member;
|
||||||
|
|
||||||
|
/** @type {object} */
|
||||||
|
#Post;
|
||||||
|
|
||||||
|
/** @type {object} */
|
||||||
|
#MemberFeedback;
|
||||||
|
|
||||||
|
/** @type {typeof Object} */
|
||||||
|
#Feedback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} deps
|
||||||
|
* @param {object} deps.Member Bookshelf Model
|
||||||
|
* @param {object} deps.Post Bookshelf Model
|
||||||
|
* @param {object} deps.MemberFeedback Bookshelf Model
|
||||||
|
* @param {object} deps.Feedback Feedback object
|
||||||
|
*/
|
||||||
|
constructor(deps) {
|
||||||
|
this.#Member = deps.Member;
|
||||||
|
this.#Post = deps.Post;
|
||||||
|
this.#MemberFeedback = deps.MemberFeedback;
|
||||||
|
this.#Feedback = deps.Feedback;
|
||||||
|
}
|
||||||
|
|
||||||
|
async add(feedback) {
|
||||||
|
await this.#MemberFeedback.add({
|
||||||
|
id: feedback.id.toHexString(),
|
||||||
|
member_id: feedback.memberId.toHexString(),
|
||||||
|
post_id: feedback.postId.toHexString(),
|
||||||
|
score: feedback.score
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async edit(feedback) {
|
||||||
|
await this.#MemberFeedback.edit({
|
||||||
|
score: feedback.score
|
||||||
|
}, {
|
||||||
|
id: feedback.id.toHexString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(postId, memberId) {
|
||||||
|
const model = await this.#MemberFeedback.findOne({member_id: memberId, post_id: postId}, {require: false});
|
||||||
|
|
||||||
|
if (!model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new this.#Feedback({
|
||||||
|
id: model.id,
|
||||||
|
memberId: model.get('member_id'),
|
||||||
|
postId: model.get('post_id'),
|
||||||
|
score: model.get('score')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMemberByUuid(uuid) {
|
||||||
|
return await this.#Member.findOne({uuid});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPostById(id) {
|
||||||
|
return await this.#Post.findOne({id});
|
||||||
|
}
|
||||||
|
};
|
28
ghost/core/core/server/services/audience-feedback/index.js
Normal file
28
ghost/core/core/server/services/audience-feedback/index.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
const FeedbackRepository = require('./FeedbackRepository');
|
||||||
|
|
||||||
|
class AudienceFeedbackServiceWrapper {
|
||||||
|
async init() {
|
||||||
|
if (this.service) {
|
||||||
|
// Already done
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire up all the dependencies
|
||||||
|
const models = require('../../models');
|
||||||
|
|
||||||
|
const {AudienceFeedbackService, AudienceFeedbackController, Feedback} = require('@tryghost/audience-feedback');
|
||||||
|
|
||||||
|
this.repository = new FeedbackRepository({
|
||||||
|
Member: models.Member,
|
||||||
|
MemberFeedback: models.MemberFeedback,
|
||||||
|
Feedback,
|
||||||
|
Post: models.Post
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expose the service
|
||||||
|
this.service = new AudienceFeedbackService();
|
||||||
|
this.controller = new AudienceFeedbackController({repository: this.repository});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new AudienceFeedbackServiceWrapper();
|
@ -5,6 +5,13 @@ const models = require('../../models');
|
|||||||
const urlUtils = require('../../../shared/url-utils');
|
const urlUtils = require('../../../shared/url-utils');
|
||||||
const spamPrevention = require('../../web/shared/middleware/api/spam-prevention');
|
const spamPrevention = require('../../web/shared/middleware/api/spam-prevention');
|
||||||
const {formattedMemberResponse} = require('./utils');
|
const {formattedMemberResponse} = require('./utils');
|
||||||
|
const errors = require('@tryghost/errors');
|
||||||
|
const tpl = require('@tryghost/tpl');
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
missingUuid: 'Missing uuid.',
|
||||||
|
invalidUuid: 'Invalid uuid.'
|
||||||
|
};
|
||||||
|
|
||||||
// @TODO: This piece of middleware actually belongs to the frontend, not to the member app
|
// @TODO: This piece of middleware actually belongs to the frontend, not to the member app
|
||||||
// Need to figure a way to separate these things (e.g. frontend actually talks to members API)
|
// Need to figure a way to separate these things (e.g. frontend actually talks to members API)
|
||||||
@ -20,6 +27,38 @@ const loadMemberSession = async function (req, res, next) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require member authentication, and make it possible to authenticate via uuid.
|
||||||
|
* You can chain this after loadMemberSession to make it possible to authetnicate via both the uuid and the session.
|
||||||
|
*/
|
||||||
|
const authMemberByUuid = async function (req, res, next) {
|
||||||
|
try {
|
||||||
|
if (res.locals.member && req.member) {
|
||||||
|
// Already authenticated via session
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const uuid = req.query.uuid;
|
||||||
|
if (!uuid) {
|
||||||
|
throw new errors.UnauthorizedError({
|
||||||
|
messsage: tpl(messages.missingUuid)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = await membersService.api.memberBREADService.read({uuid});
|
||||||
|
if (!member) {
|
||||||
|
throw new errors.UnauthorizedError({
|
||||||
|
message: tpl(messages.invalidUuid)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Object.assign(req, {member});
|
||||||
|
res.locals.member = req.member;
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getIdentityToken = async function (req, res) {
|
const getIdentityToken = async function (req, res) {
|
||||||
try {
|
try {
|
||||||
const token = await membersService.ssr.getIdentityTokenForMemberFromSession(req, res);
|
const token = await membersService.ssr.getIdentityTokenForMemberFromSession(req, res);
|
||||||
@ -216,6 +255,7 @@ const createSessionFromMagicLink = async function (req, res, next) {
|
|||||||
// Set req.member & res.locals.member if a cookie is set
|
// Set req.member & res.locals.member if a cookie is set
|
||||||
module.exports = {
|
module.exports = {
|
||||||
loadMemberSession,
|
loadMemberSession,
|
||||||
|
authMemberByUuid,
|
||||||
createSessionFromMagicLink,
|
createSessionFromMagicLink,
|
||||||
getIdentityToken,
|
getIdentityToken,
|
||||||
getMemberNewsletters,
|
getMemberNewsletters,
|
||||||
|
@ -10,6 +10,8 @@ const shared = require('../shared');
|
|||||||
const labs = require('../../../shared/labs');
|
const labs = require('../../../shared/labs');
|
||||||
const errorHandler = require('@tryghost/mw-error-handler');
|
const errorHandler = require('@tryghost/mw-error-handler');
|
||||||
const config = require('../../../shared/config');
|
const config = require('../../../shared/config');
|
||||||
|
const {http} = require('@tryghost/api-framework');
|
||||||
|
const api = require('../../api').endpoints;
|
||||||
|
|
||||||
const commentRouter = require('../comments');
|
const commentRouter = require('../comments');
|
||||||
|
|
||||||
@ -65,6 +67,16 @@ module.exports = function setupMembersApp() {
|
|||||||
// Comments
|
// Comments
|
||||||
membersApp.use('/api/comments', commentRouter());
|
membersApp.use('/api/comments', commentRouter());
|
||||||
|
|
||||||
|
// Feedback
|
||||||
|
membersApp.post(
|
||||||
|
'/api/feedback',
|
||||||
|
labs.enabledMiddleware('audienceFeedback'),
|
||||||
|
bodyParser.json({limit: '50mb'}),
|
||||||
|
middleware.loadMemberSession,
|
||||||
|
middleware.authMemberByUuid,
|
||||||
|
http(api.feedbackMembers.add)
|
||||||
|
);
|
||||||
|
|
||||||
// API error handling
|
// API error handling
|
||||||
membersApp.use('/api', errorHandler.resourceNotFound);
|
membersApp.use('/api', errorHandler.resourceNotFound);
|
||||||
membersApp.use('/api', errorHandler.handleJSONResponse(sentry));
|
membersApp.use('/api', errorHandler.handleJSONResponse(sentry));
|
||||||
|
@ -0,0 +1,265 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Members Feedback Authentication Allows authentication via session 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"feedback": Array [
|
||||||
|
Object {
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"memberId": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"postId": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"score": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members Feedback Authentication Allows authentication via session 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "*",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "132",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/feedback\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
|
||||||
|
"vary": "Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members Feedback Can add feedback 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"feedback": Array [
|
||||||
|
Object {
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"memberId": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"postId": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"type": "positive",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members Feedback Can add feedback 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "*",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "140",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/feedback\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
|
||||||
|
"vary": "Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members Feedback Can add positive feedback 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"feedback": Array [
|
||||||
|
Object {
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"memberId": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"postId": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"score": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members Feedback Can add positive feedback 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "*",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "132",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/feedback\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
|
||||||
|
"vary": "Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members Feedback Can change existing feedback 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"feedback": Array [
|
||||||
|
Object {
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"memberId": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"postId": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"score": 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members Feedback Can change existing feedback 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "*",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "132",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/feedback\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
|
||||||
|
"vary": "Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members Feedback Can change existing feedback 3: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"feedback": Array [
|
||||||
|
Object {
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"memberId": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"postId": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"score": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members Feedback Can change existing feedback 4: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "*",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "132",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/feedback\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
|
||||||
|
"vary": "Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members Feedback Can change existing feedback 5: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"feedback": Array [
|
||||||
|
Object {
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"memberId": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"postId": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"score": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members Feedback Can change existing feedback 6: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "*",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "132",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/feedback\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
|
||||||
|
"vary": "Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members Feedback Validation Throws for invalid score 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"errors": Array [
|
||||||
|
Object {
|
||||||
|
"code": null,
|
||||||
|
"context": "Invalid feedback score. Only 1 or 0 is currently allowed.",
|
||||||
|
"details": null,
|
||||||
|
"ghostErrorCode": null,
|
||||||
|
"help": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
"message": "Validation error, cannot save feedback.",
|
||||||
|
"property": null,
|
||||||
|
"type": "ValidationError",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members Feedback Validation Throws for invalid score type 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"errors": Array [
|
||||||
|
Object {
|
||||||
|
"code": null,
|
||||||
|
"context": "Invalid feedback score. Only 1 or 0 is currently allowed.",
|
||||||
|
"details": null,
|
||||||
|
"ghostErrorCode": null,
|
||||||
|
"help": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
"message": "Validation error, cannot save feedback.",
|
||||||
|
"property": null,
|
||||||
|
"type": "ValidationError",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members Feedback Validation Throws for invalid type 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"errors": Array [
|
||||||
|
Object {
|
||||||
|
"code": null,
|
||||||
|
"context": "Invalid feedback type",
|
||||||
|
"details": null,
|
||||||
|
"ghostErrorCode": null,
|
||||||
|
"help": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
"message": "Validation error, cannot save feedback.",
|
||||||
|
"property": null,
|
||||||
|
"type": "ValidationError",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members Feedback Validation Throws for invalid uuid 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"errors": Array [
|
||||||
|
Object {
|
||||||
|
"code": null,
|
||||||
|
"context": null,
|
||||||
|
"details": null,
|
||||||
|
"ghostErrorCode": null,
|
||||||
|
"help": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
"message": "Invalid uuid.",
|
||||||
|
"property": null,
|
||||||
|
"type": "UnauthorizedError",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members Feedback Validation Throws for nonexisting post 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"errors": Array [
|
||||||
|
Object {
|
||||||
|
"code": null,
|
||||||
|
"context": "Post not found.",
|
||||||
|
"details": null,
|
||||||
|
"ghostErrorCode": null,
|
||||||
|
"help": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
"message": "Resource not found error, cannot save feedback.",
|
||||||
|
"property": null,
|
||||||
|
"type": "NotFoundError",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members Feedback Validation Throws for nonexisting uuid 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"errors": Array [
|
||||||
|
Object {
|
||||||
|
"code": null,
|
||||||
|
"context": null,
|
||||||
|
"details": null,
|
||||||
|
"ghostErrorCode": null,
|
||||||
|
"help": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
"message": "Invalid uuid.",
|
||||||
|
"property": null,
|
||||||
|
"type": "UnauthorizedError",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
282
ghost/core/test/e2e-api/members/feedback.test.js
Normal file
282
ghost/core/test/e2e-api/members/feedback.test.js
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
const {agentProvider, mockManager, fixtureManager, matchers, configUtils} = require('../../utils/e2e-framework');
|
||||||
|
const {anyEtag, anyObjectId, anyLocationFor, anyErrorId} = matchers;
|
||||||
|
const models = require('../../../core/server/models');
|
||||||
|
const sinon = require('sinon');
|
||||||
|
|
||||||
|
describe('Members Feedback', function () {
|
||||||
|
let membersAgent, membersAgent2, memberUuid;
|
||||||
|
let clock;
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
membersAgent = await agentProvider.getMembersAPIAgent();
|
||||||
|
membersAgent2 = await agentProvider.getMembersAPIAgent();
|
||||||
|
|
||||||
|
await fixtureManager.init('posts', 'members');
|
||||||
|
memberUuid = fixtureManager.get('members', 0).uuid;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
mockManager.mockMail();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
clock?.restore();
|
||||||
|
clock = undefined;
|
||||||
|
configUtils.restore();
|
||||||
|
mockManager.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Authentication', function () {
|
||||||
|
it('Allows authentication via session', async function () {
|
||||||
|
const postId = fixtureManager.get('posts', 0).id;
|
||||||
|
await membersAgent2.loginAs('authenticationtest@email.com');
|
||||||
|
|
||||||
|
await membersAgent2
|
||||||
|
.post('/api/feedback/')
|
||||||
|
.body({
|
||||||
|
feedback: [{
|
||||||
|
score: 1,
|
||||||
|
post_id: postId
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.expectStatus(201)
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag,
|
||||||
|
location: anyLocationFor('feedback')
|
||||||
|
})
|
||||||
|
.matchBodySnapshot({
|
||||||
|
feedback: [
|
||||||
|
{
|
||||||
|
id: anyObjectId,
|
||||||
|
memberId: anyObjectId,
|
||||||
|
postId: anyObjectId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Validation', function () {
|
||||||
|
const postId = fixtureManager.get('posts', 0).id;
|
||||||
|
|
||||||
|
it('Throws for invalid score', async function () {
|
||||||
|
await membersAgent
|
||||||
|
.post(`/api/feedback/?uuid=${memberUuid}`)
|
||||||
|
.body({
|
||||||
|
feedback: [{
|
||||||
|
score: 2,
|
||||||
|
post_id: postId
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.expectStatus(422)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
id: anyErrorId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Throws for invalid score type', async function () {
|
||||||
|
await membersAgent
|
||||||
|
.post(`/api/feedback/?uuid=${memberUuid}`)
|
||||||
|
.body({
|
||||||
|
feedback: [{
|
||||||
|
score: 'text',
|
||||||
|
post_id: postId
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.expectStatus(422)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
id: anyErrorId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Throws for invalid uuid', async function () {
|
||||||
|
await membersAgent
|
||||||
|
.post(`/api/feedback/?uuid=1234`)
|
||||||
|
.body({
|
||||||
|
feedback: [{
|
||||||
|
score: 1,
|
||||||
|
post_id: postId
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.expectStatus(401)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
id: anyErrorId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Throws for nonexisting uuid', async function () {
|
||||||
|
const uuid = '00000000-0000-0000-0000-000000000000';
|
||||||
|
await membersAgent
|
||||||
|
.post(`/api/feedback/?uuid=${uuid}`)
|
||||||
|
.body({
|
||||||
|
feedback: [{
|
||||||
|
score: 1,
|
||||||
|
post_id: postId
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.expectStatus(401)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
id: anyErrorId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Throws for nonexisting post', async function () {
|
||||||
|
await membersAgent
|
||||||
|
.post(`/api/feedback/?uuid=${memberUuid}`)
|
||||||
|
.body({
|
||||||
|
feedback: [{
|
||||||
|
score: 1,
|
||||||
|
post_id: '123'
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.expectStatus(404)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
id: anyErrorId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can add positive feedback', async function () {
|
||||||
|
const postId = fixtureManager.get('posts', 0).id;
|
||||||
|
|
||||||
|
await membersAgent
|
||||||
|
.post(`/api/feedback/?uuid=${memberUuid}`)
|
||||||
|
.body({
|
||||||
|
feedback: [{
|
||||||
|
score: 1,
|
||||||
|
post_id: postId
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.expectStatus(201)
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag,
|
||||||
|
location: anyLocationFor('feedback')
|
||||||
|
})
|
||||||
|
.matchBodySnapshot({
|
||||||
|
feedback: [
|
||||||
|
{
|
||||||
|
id: anyObjectId,
|
||||||
|
memberId: anyObjectId,
|
||||||
|
postId: anyObjectId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can change existing feedback', async function () {
|
||||||
|
clock = sinon.useFakeTimers(new Date());
|
||||||
|
const postId = fixtureManager.get('posts', 1).id;
|
||||||
|
|
||||||
|
const {body} = await membersAgent
|
||||||
|
.post(`/api/feedback/?uuid=${memberUuid}`)
|
||||||
|
.body({
|
||||||
|
feedback: [{
|
||||||
|
score: 0,
|
||||||
|
post_id: postId
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.expectStatus(201)
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag,
|
||||||
|
location: anyLocationFor('feedback')
|
||||||
|
})
|
||||||
|
.matchBodySnapshot({
|
||||||
|
feedback: [
|
||||||
|
{
|
||||||
|
id: anyObjectId,
|
||||||
|
memberId: anyObjectId,
|
||||||
|
postId: anyObjectId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(body.feedback[0].score, 0);
|
||||||
|
const feedbackId = body.feedback[0].id;
|
||||||
|
|
||||||
|
// Fetch real model
|
||||||
|
const model1 = await models.MemberFeedback.findOne({id: feedbackId}, {require: true});
|
||||||
|
|
||||||
|
clock.tick(10 * 60 * 1000);
|
||||||
|
|
||||||
|
const {body: body2} = await membersAgent
|
||||||
|
.post(`/api/feedback/?uuid=${memberUuid}`)
|
||||||
|
.body({
|
||||||
|
feedback: [{
|
||||||
|
score: 1,
|
||||||
|
post_id: postId
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.expectStatus(201)
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag,
|
||||||
|
location: anyLocationFor('feedback')
|
||||||
|
})
|
||||||
|
.matchBodySnapshot({
|
||||||
|
feedback: [
|
||||||
|
{
|
||||||
|
id: anyObjectId,
|
||||||
|
memberId: anyObjectId,
|
||||||
|
postId: anyObjectId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(body2.feedback[0].id, feedbackId);
|
||||||
|
assert.equal(body2.feedback[0].score, 1);
|
||||||
|
const model2 = await models.MemberFeedback.findOne({id: feedbackId}, {require: true});
|
||||||
|
|
||||||
|
assert.notEqual(model2.get('updated_at').getTime(), model1.get('updated_at').getTime());
|
||||||
|
|
||||||
|
clock.tick(10 * 60 * 1000);
|
||||||
|
|
||||||
|
// Do the same change again, and the model shouldn't change
|
||||||
|
const {body: body3} = await membersAgent
|
||||||
|
.post(`/api/feedback/?uuid=${memberUuid}`)
|
||||||
|
.body({
|
||||||
|
feedback: [{
|
||||||
|
score: 1,
|
||||||
|
post_id: postId
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.expectStatus(201)
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag,
|
||||||
|
location: anyLocationFor('feedback')
|
||||||
|
})
|
||||||
|
.matchBodySnapshot({
|
||||||
|
feedback: [
|
||||||
|
{
|
||||||
|
id: anyObjectId,
|
||||||
|
memberId: anyObjectId,
|
||||||
|
postId: anyObjectId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const model3 = await models.MemberFeedback.findOne({id: feedbackId}, {require: true});
|
||||||
|
assert.equal(body3.feedback[0].id, feedbackId);
|
||||||
|
assert.equal(body3.feedback[0].score, 1);
|
||||||
|
assert.equal(model3.get('updated_at').getTime(), model2.get('updated_at').getTime());
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user