1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

Feat/pnps feedback (#862)

* feat: setup user feedback service

* fix: map rows

* feat: add tests

* wrap service calls in try catch

* fix: add test for retrieving feedback on user

* feat: add fake user feedback store

* fix: check ffor feedback id in controller

* feat: add test for bad request
This commit is contained in:
Fredrik Strand Oseberg 2021-06-07 11:11:42 +02:00 committed by GitHub
parent 3858b29d80
commit 9f33285b03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 370 additions and 7 deletions

View File

@ -1,7 +1,7 @@
'use strict';
// eslint-disable-next-line
import EventEmitter from "events";
import EventEmitter from 'events';
import { IUnleashConfig } from '../types/option';
import { IUnleashStores } from '../types/stores';
@ -25,6 +25,7 @@ import { ApiTokenStore } from './api-token-store';
import SessionStore from './session-store';
import { AccessStore } from './access-store';
import { ResetTokenStore } from './reset-token-store';
import UserFeedbackStore from './user-feedback-store';
export const createStores = (
config: IUnleashConfig,
@ -59,6 +60,7 @@ export const createStores = (
apiTokenStore: new ApiTokenStore(db, eventBus, getLogger),
resetTokenStore: new ResetTokenStore(db, eventBus, getLogger),
sessionStore: new SessionStore(db, eventBus, getLogger),
userFeedbackStore: new UserFeedbackStore(db, eventBus, getLogger),
};
};

View File

@ -0,0 +1,86 @@
'use strict';
import { Knex } from 'knex';
import { EventEmitter } from 'events';
import { LogProvider, Logger } from '../logger';
const COLUMNS = ['given', 'user_id', 'feedback_id', 'nevershow'];
const TABLE = 'user_feedback';
interface IUserFeedbackTable {
nevershow?: boolean;
feedback_id: string;
given?: Date;
user_id: number;
}
export interface IUserFeedback {
neverShow: boolean;
feedbackId: string;
given?: Date;
userId: number;
}
const fieldToRow = (fields: IUserFeedback): IUserFeedbackTable => {
return {
nevershow: fields.neverShow,
feedback_id: fields.feedbackId,
given: fields.given,
user_id: fields.userId,
};
};
const rowToField = (row: IUserFeedbackTable): IUserFeedback => {
return {
neverShow: row.nevershow,
feedbackId: row.feedback_id,
given: row.given,
userId: row.user_id,
};
};
export default class UserFeedbackStore {
private db: Knex;
private logger: Logger;
constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) {
this.db = db;
this.logger = getLogger('user-feedback-store.js');
}
async getAllUserFeedback(userId: number): Promise<IUserFeedback[]> {
const userFeedback = await this.db
.table<IUserFeedbackTable>(TABLE)
.select()
.where({ user_id: userId });
return userFeedback.map(rowToField);
}
async getFeedback(
userId: number,
feedbackId: string,
): Promise<IUserFeedback> {
const userFeedback = await this.db
.table<IUserFeedbackTable>(TABLE)
.select()
.where({ user_id: userId, feedback_id: feedbackId })
.first();
return rowToField(userFeedback);
}
async updateFeedback(feedback: IUserFeedback): Promise<IUserFeedback> {
const insertedFeedback = await this.db
.table<IUserFeedbackTable>(TABLE)
.insert(fieldToRow(feedback))
.onConflict(['user_id', 'feedback_id'])
.merge()
.returning(COLUMNS);
return rowToField(insertedFeedback[0]);
}
}
module.exports = UserFeedbackStore;

View File

@ -19,6 +19,7 @@ import AddonController from './addon';
import ApiTokenController from './api-token-controller';
import UserAdminController from './user-admin';
import EmailController from './email';
import UserFeedbackController from './user-feedback-controller';
class AdminApi extends Controller {
constructor(config: IUnleashConfig, services: IUnleashServices) {
@ -75,6 +76,10 @@ class AdminApi extends Controller {
'/user-admin',
new UserAdminController(config, services).router,
);
this.app.use(
'/feedback',
new UserFeedbackController(config, services).router,
);
}
index(req, res) {

View File

@ -0,0 +1,93 @@
import { Response } from 'express';
import Controller from '../controller';
import { Logger } from '../../logger';
import { IUserRequest } from './user';
import { IUnleashConfig } from '../../types/option';
import { IUnleashServices } from '../../types/services';
import UserFeedbackService from '../../services/user-feedback-service';
import { handleErrors } from './util';
interface IFeedbackBody {
neverShow?: boolean;
feedbackId: string;
given?: Date;
}
class UserFeedbackController extends Controller {
private logger: Logger;
private userFeedbackService: UserFeedbackService;
constructor(
config: IUnleashConfig,
{ userFeedbackService }: Pick<IUnleashServices, 'userFeedbackService'>,
) {
super(config);
this.logger = config.getLogger('feedback-controller.ts');
this.userFeedbackService = userFeedbackService;
this.post('/', this.recordFeedback);
this.put('/:id', this.updateFeedbackSettings);
}
private async recordFeedback(
req: IUserRequest<any, any, IFeedbackBody, any>,
res: Response,
): Promise<void> {
const BAD_REQUEST = 400;
const { user } = req;
const { feedbackId } = req.body;
if (!feedbackId) {
res.status(BAD_REQUEST).json({
error: 'feedbackId must be present.',
});
return;
}
const feedback = {
...req.body,
userId: user.id,
given: new Date(),
neverShow: req.body.neverShow || false,
};
try {
const updated = await this.userFeedbackService.updateFeedback(
feedback,
);
res.json(updated);
} catch (e) {
handleErrors(res, this.logger, e);
}
}
private async updateFeedbackSettings(
req: IUserRequest<any, any, IFeedbackBody, any>,
res: Response,
): Promise<void> {
const { user } = req;
const { id } = req.params;
const feedback = {
...req.body,
feedbackId: id,
userId: user.id,
neverShow: req.body.neverShow || false,
};
try {
const updated = await this.userFeedbackService.updateFeedback(
feedback,
);
res.json(updated);
} catch (e) {
handleErrors(res, this.logger, e);
}
}
}
module.exports = UserFeedbackController;
export default UserFeedbackController;

View File

@ -11,13 +11,14 @@ import User from '../../types/user';
import { Logger } from '../../logger';
import { handleErrors } from './util';
import SessionService from '../../services/session-service';
import UserFeedbackService from '../../services/user-feedback-service';
interface IChangeUserRequest {
password: string;
confirmPassword: string;
}
interface UserRequest<PARAM, QUERY, BODY, RESPONSE>
export interface IUserRequest<PARAM, QUERY, BODY, RESPONSE>
extends Request<PARAM, QUERY, BODY, RESPONSE> {
user: User;
}
@ -27,6 +28,8 @@ class UserController extends Controller {
private userService: UserService;
private userFeedbackService: UserFeedbackService;
private sessionService: SessionService;
private logger: Logger;
@ -37,15 +40,20 @@ class UserController extends Controller {
accessService,
userService,
sessionService,
userFeedbackService,
}: Pick<
IUnleashServices,
'accessService' | 'userService' | 'sessionService'
IUnleashServices,
| 'accessService'
| 'userService'
| 'sessionService'
| 'userFeedbackService'
>,
) {
super(config);
this.accessService = accessService;
this.userService = userService;
this.sessionService = sessionService;
this.userFeedbackService = userFeedbackService;
this.logger = config.getLogger('lib/routes/admin-api/user.ts');
this.get('/', this.getUser);
@ -60,17 +68,21 @@ class UserController extends Controller {
const permissions = await this.accessService.getPermissionsForUser(
user,
);
const feedback = await this.userFeedbackService.getAllUserFeedback(
user.id,
);
delete user.permissions; // TODO: remove
return res
.status(200)
.json({ user, permissions })
.json({ user, permissions, feedback })
.end();
}
return res.status(404).end();
}
async updateUserPass(
req: UserRequest<any, any, IChangeUserRequest, any>,
req: IUserRequest<any, any, IChangeUserRequest, any>,
res: Response,
): Promise<void> {
const { user } = req;
@ -93,7 +105,7 @@ class UserController extends Controller {
}
async mySessions(
req: UserRequest<any, any, any, any>,
req: IUserRequest<any, any, any, any>,
res: Response,
): Promise<void> {
const { user } = req;

View File

@ -22,6 +22,7 @@ import UserService from './user-service';
import ResetTokenService from './reset-token-service';
import SettingService from './setting-service';
import SessionService from './session-service';
import UserFeedbackService from './user-feedback-service';
export const createServices = (
stores: IUnleashStores,
@ -52,6 +53,7 @@ export const createServices = (
const versionService = new VersionService(stores, config);
const healthService = new HealthService(stores, config);
const settingService = new SettingService(stores, config);
const userFeedbackService = new UserFeedbackService(stores, config);
return {
accessService,
@ -74,6 +76,7 @@ export const createServices = (
eventService,
settingService,
sessionService,
userFeedbackService,
};
};

View File

@ -0,0 +1,35 @@
import { Logger } from '../logger';
import UserFeedbackStore, { IUserFeedback } from '../db/user-feedback-store';
import { IUnleashStores } from '../types/stores';
import { IUnleashConfig } from '../types/option';
export default class UserFeedbackService {
private userFeedbackStore: UserFeedbackStore;
private logger: Logger;
constructor(
{ userFeedbackStore }: Pick<IUnleashStores, 'userFeedbackStore'>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
) {
this.userFeedbackStore = userFeedbackStore;
this.logger = getLogger('services/user-feedback-service.js');
}
async getAllUserFeedback(user_id: number): Promise<IUserFeedback[]> {
return this.userFeedbackStore.getAllUserFeedback(user_id);
}
async getFeedback(
user_id: number,
feedback_id: string,
): Promise<IUserFeedback> {
return this.userFeedbackStore.getFeedback(user_id, feedback_id);
}
async updateFeedback(feedback: IUserFeedback): Promise<IUserFeedback> {
return this.userFeedbackStore.updateFeedback(feedback);
}
}
module.exports = UserFeedbackService;

View File

@ -18,6 +18,7 @@ import EventService from '../services/event-service';
import HealthService from '../services/health-service';
import SettingService from '../services/setting-service';
import SessionService from '../services/session-service';
import UserFeedbackService from '../services/user-feedback-service';
export interface IUnleashServices {
accessService: AccessService;
@ -40,4 +41,5 @@ export interface IUnleashServices {
tagService: TagService;
userService: UserService;
versionService: VersionService;
userFeedbackService: UserFeedbackService;
}

View File

@ -13,6 +13,7 @@ import UserStore from '../db/user-store';
import TagStore from '../db/tag-store';
import TagTypeStore from '../db/tag-type-store';
import AddonStore from '../db/addon-store';
import UserFeedbackStore from '../db/user-feedback-store';
import { AccessStore } from '../db/access-store';
import { ApiTokenStore } from '../db/api-token-store';
import { ResetTokenStore } from '../db/reset-token-store';
@ -37,5 +38,6 @@ export interface IUnleashStores {
accessStore: AccessStore;
apiTokenStore: ApiTokenStore;
resetTokenStore: ResetTokenStore;
userFeedbackStore: UserFeedbackStore;
db: Knex;
}

View File

@ -0,0 +1,26 @@
'use strict';
exports.up = function(db, cb) {
db.runSql(
`
CREATE TABLE IF NOT EXISTS user_feedback
(user_id INTEGER NOT NULL references users (id),
feedback_id TEXT,
given TIMESTAMP WITH TIME ZONE,
neverShow BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (user_id, feedback_id));
CREATE INDEX user_feedback_user_id_idx ON user_feedback (user_id);
`,
cb,
);
};
exports.down = function(db, cb) {
db.runSql(
`
DROP INDEX user_feedback_user_id_idx;
DROP TABLE user_feedback;
`,
cb,
);
};

View File

@ -0,0 +1,90 @@
import { setupAppWithCustomAuth } from '../../helpers/test-helper';
import dbInit from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger';
import { IUnleashConfig } from '../../../../lib/types/option';
import { IUnleashServices } from '../../../../lib/types/services';
let stores;
let db;
let app;
beforeAll(async () => {
db = await dbInit('feedback_api_serial', getLogger);
stores = db.stores;
const email = 'custom-user@mail.com';
const preHook = (
app: any,
config: IUnleashConfig,
{ userService }: IUnleashServices,
) => {
app.use('/api/admin/', async (req, res, next) => {
req.user = await userService.loginUserWithoutPassword(email, true);
next();
});
};
app = await setupAppWithCustomAuth(stores, preHook);
});
afterAll(async () => {
await app.destroy();
await db.destroy();
});
test('it creates feedback for user', async () => {
expect.assertions(1);
return app.request
.post('/api/admin/feedback')
.send({ feedbackId: 'pnps' })
.set('Content-Type', 'application/json')
.expect('Content-Type', /json/)
.expect(200)
.expect(res => {
expect(res.body.feedbackId).toBe('pnps');
});
});
test('it gives 400 when feedback is not present', async () => {
expect.assertions(1);
return app.request
.post('/api/admin/feedback')
.send({})
.set('Content-Type', 'application/json')
.expect('Content-Type', /json/)
.expect(400)
.expect(res => {
expect(res.body.error).toBeTruthy();
});
});
test('it updates feedback for user', async () => {
expect.assertions(1);
return app.request
.put('/api/admin/feedback/pnps')
.send({ neverShow: true })
.set('Content-Type', 'application/json')
.expect('Content-Type', /json/)
.expect(200)
.expect(res => {
expect(res.body.neverShow).toBe(true);
});
});
test('it retrieves feedback for user', async () => {
expect.assertions(2);
return app.request
.get('/api/admin/user')
.set('Content-Type', 'application/json')
.expect('Content-Type', /json/)
.expect(200)
.expect(res => {
expect(res.body.feedback.length).toBe(1);
expect(res.body.feedback[0].feedbackId).toBe('pnps');
});
});

View File

@ -0,0 +1,5 @@
module.exports = () => ({
getAllUserFeedback: () => Promise.resolve([]),
getFeedback: () => Promise.resolve({}),
updateFeedback: () => Promise.resolve({}),
});

View File

@ -14,6 +14,7 @@ const addonStore = require('./fake-addon-store');
const projectStore = require('./fake-project-store');
const UserStore = require('./fake-user-store');
const AccessStore = require('./fake-access-store');
const userFeedbackStore = require('./fake-user-feedback-store');
module.exports = {
createStores: (databaseIsUp = true) => {
@ -39,6 +40,7 @@ module.exports = {
projectStore: projectStore(databaseIsUp),
userStore: new UserStore(),
accessStore: new AccessStore(),
userFeedbackStore: userFeedbackStore(databaseIsUp),
};
},
};