From 69727e78df977e06e3c35f6d235ff63f529839ae Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 14 Feb 2024 13:06:29 +0100 Subject: [PATCH 1/7] fix: Throw not found error for authentication --- packages/backend/src/helpers/authentication.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/helpers/authentication.js b/packages/backend/src/helpers/authentication.js index 010856ee..42708a0b 100644 --- a/packages/backend/src/helpers/authentication.js +++ b/packages/backend/src/helpers/authentication.js @@ -20,7 +20,8 @@ export const isAuthenticated = async (_parent, _args, req) => { .withGraphFetched({ role: true, permissions: true, - }); + }) + .throwIfNotFound(); return true; } catch (error) { From b599466ffadcae8bc2db802ad80735457b18ffcf Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 14 Feb 2024 13:06:58 +0100 Subject: [PATCH 2/7] feat: Add checkIsCloud middleware for routes --- packages/backend/src/helpers/check-is-cloud.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 packages/backend/src/helpers/check-is-cloud.js diff --git a/packages/backend/src/helpers/check-is-cloud.js b/packages/backend/src/helpers/check-is-cloud.js new file mode 100644 index 00000000..f0b93b56 --- /dev/null +++ b/packages/backend/src/helpers/check-is-cloud.js @@ -0,0 +1,11 @@ +import appConfig from '../config/app.js'; + +export const checkIsCloud = async (request, response, next) => { + if (appConfig.isCloud) { + next(); + } else { + return response.status(404).end(); + } +}; + +export default checkIsCloud; From 0e111a3532b2ee5a9f610ac22943910d4f702ab9 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 14 Feb 2024 14:18:28 +0100 Subject: [PATCH 3/7] feat: Implement API endpoint for user trial info --- .../controllers/api/v1/users/get-user-trial.js | 16 ++++++++++++++++ packages/backend/src/routes/api/v1/users.js | 8 ++++++++ 2 files changed, 24 insertions(+) create mode 100644 packages/backend/src/controllers/api/v1/users/get-user-trial.js diff --git a/packages/backend/src/controllers/api/v1/users/get-user-trial.js b/packages/backend/src/controllers/api/v1/users/get-user-trial.js new file mode 100644 index 00000000..8457de43 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-user-trial.js @@ -0,0 +1,16 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const inTrial = await request.currentUser.inTrial(); + + const hasActiveSubscription = + await request.currentUser.hasActiveSubscription(); + + const trialInfo = { + inTrial, + hasActiveSubscription, + expireAt: request.currentUser.trialExpiryDate, + }; + + renderObject(response, trialInfo); +}; diff --git a/packages/backend/src/routes/api/v1/users.js b/packages/backend/src/routes/api/v1/users.js index fb1ece18..0b5fd2ab 100644 --- a/packages/backend/src/routes/api/v1/users.js +++ b/packages/backend/src/routes/api/v1/users.js @@ -1,14 +1,22 @@ import { Router } from 'express'; import { authenticateUser } from '../../../helpers/authentication.js'; import { authorizeUser } from '../../../helpers/authorization.js'; +import checkIsCloud from '../../../helpers/check-is-cloud.js'; import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.js'; import getUserAction from '../../../controllers/api/v1/users/get-user.js'; import getUsersAction from '../../../controllers/api/v1/users/get-users.js'; +import getUserTrialAction from '../../../controllers/api/v1/users/get-user-trial.js'; const router = Router(); router.get('/', authenticateUser, authorizeUser, getUsersAction); router.get('/me', authenticateUser, getCurrentUserAction); router.get('/:userId', authenticateUser, authorizeUser, getUserAction); +router.get( + '/:userId/trial', + authenticateUser, + checkIsCloud, + getUserTrialAction +); export default router; From a591d0ea878966613aa4be882f377363324102f1 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 14 Feb 2024 14:18:42 +0100 Subject: [PATCH 4/7] test: Add tests for get user trial action --- .../api/v1/users/get-user-trial.test.js | 38 +++++++++++++++++++ .../mocks/rest/api/v1/users/get-user-trial.js | 18 +++++++++ 2 files changed, 56 insertions(+) create mode 100644 packages/backend/src/controllers/api/v1/users/get-user-trial.test.js create mode 100644 packages/backend/test/mocks/rest/api/v1/users/get-user-trial.js diff --git a/packages/backend/src/controllers/api/v1/users/get-user-trial.test.js b/packages/backend/src/controllers/api/v1/users/get-user-trial.test.js new file mode 100644 index 00000000..3cd8de9b --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-user-trial.test.js @@ -0,0 +1,38 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import getUserTrialMock from '../../../../../test/mocks/rest/api/v1/users/get-user-trial'; +import appConfig from '../../../../config/app'; +import { DateTime } from 'luxon'; +import User from '../../../../models/user'; + +describe('GET /api/v1/users/:userId/trial', () => { + let user, token; + + beforeEach(async () => { + const trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate(); + user = await createUser({ trialExpiryDate }); + token = createAuthTokenByUserId(user.id); + + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true); + }); + + describe('should return in trial, active subscription and expire at info', () => { + beforeEach(async () => { + vi.spyOn(User.prototype, 'inTrial').mockResolvedValue(false); + vi.spyOn(User.prototype, 'hasActiveSubscription').mockResolvedValue(true); + }); + + it('should return null', async () => { + const response = await request(app) + .get(`/api/v1/users/${user.id}/trial`) + .set('Authorization', token) + .expect(200); + + const expectedResponsePayload = await getUserTrialMock(user); + expect(response.body).toEqual(expectedResponsePayload); + }); + }); +}); diff --git a/packages/backend/test/mocks/rest/api/v1/users/get-user-trial.js b/packages/backend/test/mocks/rest/api/v1/users/get-user-trial.js new file mode 100644 index 00000000..297cb23a --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/users/get-user-trial.js @@ -0,0 +1,18 @@ +const getUserTrialMock = async (currentUser) => { + return { + data: { + inTrial: await currentUser.inTrial(), + hasActiveSubscription: await currentUser.hasActiveSubscription(), + expireAt: currentUser.trialExpiryDate.toISOString(), + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default getUserTrialMock; From 0afcdce6d35ff4a56178027f9647da2dc3089329 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 14 Feb 2024 14:23:54 +0100 Subject: [PATCH 5/7] refactor: Do not expose subscription info for get user trial --- .../backend/src/controllers/api/v1/users/get-user-trial.js | 4 ---- .../backend/test/mocks/rest/api/v1/users/get-user-trial.js | 1 - 2 files changed, 5 deletions(-) diff --git a/packages/backend/src/controllers/api/v1/users/get-user-trial.js b/packages/backend/src/controllers/api/v1/users/get-user-trial.js index 8457de43..1c4575bb 100644 --- a/packages/backend/src/controllers/api/v1/users/get-user-trial.js +++ b/packages/backend/src/controllers/api/v1/users/get-user-trial.js @@ -3,12 +3,8 @@ import { renderObject } from '../../../../helpers/renderer.js'; export default async (request, response) => { const inTrial = await request.currentUser.inTrial(); - const hasActiveSubscription = - await request.currentUser.hasActiveSubscription(); - const trialInfo = { inTrial, - hasActiveSubscription, expireAt: request.currentUser.trialExpiryDate, }; diff --git a/packages/backend/test/mocks/rest/api/v1/users/get-user-trial.js b/packages/backend/test/mocks/rest/api/v1/users/get-user-trial.js index 297cb23a..7721aaf7 100644 --- a/packages/backend/test/mocks/rest/api/v1/users/get-user-trial.js +++ b/packages/backend/test/mocks/rest/api/v1/users/get-user-trial.js @@ -2,7 +2,6 @@ const getUserTrialMock = async (currentUser) => { return { data: { inTrial: await currentUser.inTrial(), - hasActiveSubscription: await currentUser.hasActiveSubscription(), expireAt: currentUser.trialExpiryDate.toISOString(), }, meta: { From 75aeff189896acfc3e57372ad8bc7533bf061031 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 14 Feb 2024 15:48:49 +0100 Subject: [PATCH 6/7] test: Cover removed user token for authentication tests --- .../src/helpers/authentication.test.js | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/packages/backend/src/helpers/authentication.test.js b/packages/backend/src/helpers/authentication.test.js index 80ce8c18..691a7628 100644 --- a/packages/backend/src/helpers/authentication.test.js +++ b/packages/backend/src/helpers/authentication.test.js @@ -1,11 +1,8 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { allow } from 'graphql-shield'; -import jwt from 'jsonwebtoken'; -import User from '../models/user.js'; import { isAuthenticated, authenticationRules } from './authentication.js'; - -vi.mock('jsonwebtoken'); -vi.mock('../models/user.js'); +import { createUser } from '../../test/factories/user.js'; +import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js'; describe('isAuthenticated', () => { it('should return false if no token is provided', async () => { @@ -14,29 +11,26 @@ describe('isAuthenticated', () => { }); it('should return false if token is invalid', async () => { - jwt.verify.mockImplementation(() => { - throw new Error('invalid token'); - }); - const req = { headers: { authorization: 'invalidToken' } }; expect(await isAuthenticated(null, null, req)).toBe(false); }); - it('should return true if token is valid', async () => { - jwt.verify.mockReturnValue({ userId: '123' }); + it('should return true if token is valid and there is a user', async () => { + const user = await createUser(); + const token = createAuthTokenByUserId(user.id); - User.query.mockReturnValue({ - findById: vi.fn().mockReturnValue({ - leftJoinRelated: vi.fn().mockReturnThis(), - withGraphFetched: vi - .fn() - .mockResolvedValue({ id: '123', role: {}, permissions: {} }), - }), - }); - - const req = { headers: { authorization: 'validToken' } }; + const req = { headers: { authorization: token } }; expect(await isAuthenticated(null, null, req)).toBe(true); }); + + it('should return false if token is valid and but there is no user', async () => { + const user = await createUser(); + const token = createAuthTokenByUserId(user.id); + await user.$query().delete(); + + const req = { headers: { authorization: token } }; + expect(await isAuthenticated(null, null, req)).toBe(false); + }); }); describe('authentication rules', () => { From 5f9786a2c7e6c001fddbfacc091904068e5110f1 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 14 Feb 2024 15:52:02 +0100 Subject: [PATCH 7/7] chore: Adjust get user trial file to have .ee extension --- .../users/{get-user-trial.js => get-user-trial.ee.js} | 0 ...et-user-trial.test.js => get-user-trial.ee.test.js} | 10 +++++----- packages/backend/src/routes/api/v1/users.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename packages/backend/src/controllers/api/v1/users/{get-user-trial.js => get-user-trial.ee.js} (100%) rename packages/backend/src/controllers/api/v1/users/{get-user-trial.test.js => get-user-trial.ee.test.js} (84%) diff --git a/packages/backend/src/controllers/api/v1/users/get-user-trial.js b/packages/backend/src/controllers/api/v1/users/get-user-trial.ee.js similarity index 100% rename from packages/backend/src/controllers/api/v1/users/get-user-trial.js rename to packages/backend/src/controllers/api/v1/users/get-user-trial.ee.js diff --git a/packages/backend/src/controllers/api/v1/users/get-user-trial.test.js b/packages/backend/src/controllers/api/v1/users/get-user-trial.ee.test.js similarity index 84% rename from packages/backend/src/controllers/api/v1/users/get-user-trial.test.js rename to packages/backend/src/controllers/api/v1/users/get-user-trial.ee.test.js index 3cd8de9b..52e300b6 100644 --- a/packages/backend/src/controllers/api/v1/users/get-user-trial.test.js +++ b/packages/backend/src/controllers/api/v1/users/get-user-trial.ee.test.js @@ -1,12 +1,12 @@ import { vi, describe, it, expect, beforeEach } from 'vitest'; import request from 'supertest'; import app from '../../../../app.js'; -import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; -import { createUser } from '../../../../../test/factories/user'; -import getUserTrialMock from '../../../../../test/mocks/rest/api/v1/users/get-user-trial'; -import appConfig from '../../../../config/app'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import getUserTrialMock from '../../../../../test/mocks/rest/api/v1/users/get-user-trial.js'; +import appConfig from '../../../../config/app.js'; import { DateTime } from 'luxon'; -import User from '../../../../models/user'; +import User from '../../../../models/user.js'; describe('GET /api/v1/users/:userId/trial', () => { let user, token; diff --git a/packages/backend/src/routes/api/v1/users.js b/packages/backend/src/routes/api/v1/users.js index 0b5fd2ab..97eb8895 100644 --- a/packages/backend/src/routes/api/v1/users.js +++ b/packages/backend/src/routes/api/v1/users.js @@ -5,7 +5,7 @@ import checkIsCloud from '../../../helpers/check-is-cloud.js'; import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.js'; import getUserAction from '../../../controllers/api/v1/users/get-user.js'; import getUsersAction from '../../../controllers/api/v1/users/get-users.js'; -import getUserTrialAction from '../../../controllers/api/v1/users/get-user-trial.js'; +import getUserTrialAction from '../../../controllers/api/v1/users/get-user-trial.ee.js'; const router = Router();