From d18463910949656931668c316fe2f34dfe324e8c Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Mon, 12 Oct 2020 18:05:38 +0800 Subject: [PATCH 01/20] create backend user on auth --- src/graphql/models/User.js | 32 +++- src/graphql/mutations/CreateOrUpdateUser.js | 78 +++++++++ .../__fixtures__/CreateOrUpdateUser.js | 7 + .../mutations/__tests__/CreateOrUpdateUser.js | 151 ++++++++++++++++++ .../__snapshots__/CreateOrUpdateUser.js.snap | 71 ++++++++ src/graphql/schema.js | 2 + src/index.js | 35 ++-- src/scripts/__tests__/fetchStatsFromGA.js | 1 - .../__tests__/createBackendUsers.js | 1 + src/scripts/migrations/createBackendUsers.js | 2 +- 10 files changed, 358 insertions(+), 22 deletions(-) create mode 100644 src/graphql/mutations/CreateOrUpdateUser.js create mode 100644 src/graphql/mutations/__fixtures__/CreateOrUpdateUser.js create mode 100644 src/graphql/mutations/__tests__/CreateOrUpdateUser.js create mode 100644 src/graphql/mutations/__tests__/__snapshots__/CreateOrUpdateUser.js.snap diff --git a/src/graphql/models/User.js b/src/graphql/models/User.js index 13fab6e3..a41c8ad9 100644 --- a/src/graphql/models/User.js +++ b/src/graphql/models/User.js @@ -39,6 +39,9 @@ export const AvatarTypes = { OpenPeeps: 'OpenPeeps', }; +export const isBackendApp = appId => + appId !== 'WEBSITE' && appId !== 'DEVELOPMENT_FRONTEND'; + /** * Generates data for open peeps avatar. */ @@ -70,6 +73,7 @@ export const encodeAppId = appId => .digest('base64') .replace(/[+/]/g, '') .substr(0, 5); + export const sha256 = value => crypto .createHash('sha256') @@ -84,7 +88,7 @@ export const sha256 = value => * @param {string} appId - app ID * @returns {string} the id used to index `user` in db */ -export const convertAppUserIdToUserId = (appId, appUserId) => { +export const convertAppUserIdToUserId = ({ appId, appUserId }) => { return `${encodeAppId(appId)}_${sha256(appUserId)}`; }; @@ -129,6 +133,7 @@ const User = new GraphQLObjectType({ email: currentUserOnlyField(GraphQLString), name: { type: GraphQLString }, avatarUrl: avatarResolver(), + // avatarData: { type: GraphQLString }, facebookId: currentUserOnlyField(GraphQLString), githubId: currentUserOnlyField(GraphQLString), @@ -194,21 +199,34 @@ const User = new GraphQLObjectType({ }, createdAt: { type: GraphQLString }, updatedAt: { type: GraphQLString }, + // lastActiveAt: { type: GraphQLString }, }), }); export default User; -export const userFieldResolver = ( +export const userFieldResolver = async ( { userId, appId }, args, { loaders, ...context } ) => { - // If the root document is created by website users, we can resolve user from userId. - // - if (appId === 'WEBSITE') - return loaders.docLoader.load({ index: 'users', id: userId }); - + // If the root document is created by website users or if the userId is already converted to db userId, + // we can resolve user from userId. + // + if (!isBackendApp(appId)) + return await loaders.docLoader.load({ index: 'users', id: userId }); + + /* + if (userId && userId.substr(0, 6) === `${encodeAppId(appId)}_`) { + return await loaders.docLoader.load({ index: 'users', id: userId }); + } + + + const user = await loaders.docLoader.load({ index: 'users', id: convertAppUserIdToUserId({ appId, appUserId: userId }) }); + if (user) return user; + */ + + // Todo: some unit tests are depending on this code block, need to clean up those tests and then remove the following lines. // If the user comes from the same client as the root document, return the user id. // if (context.appId === appId) return { id: userId }; diff --git a/src/graphql/mutations/CreateOrUpdateUser.js b/src/graphql/mutations/CreateOrUpdateUser.js new file mode 100644 index 00000000..bf71dfcf --- /dev/null +++ b/src/graphql/mutations/CreateOrUpdateUser.js @@ -0,0 +1,78 @@ +import { assertUser } from 'graphql/util'; +import User, { + generatePseudonym, + generateOpenPeepsAvatar, + AvatarTypes, + isBackendApp, + convertAppUserIdToUserId, +} from 'graphql/models/User'; +import client, { processMeta } from 'util/client'; + +import rollbar from 'rollbarInstance'; + +/** + * Index backend user if not existed, and record the last active time as now. + * + * @param {string} appUserId - user ID given by an backend app + * @param {string} appId - app ID + * + * @returns {user: User, isCreated: boolean} + */ +export async function createOrUpdateBackendUser({ appUserId, appId }) { + assertUser({ appId, userId: appUserId }); + if (!isBackendApp(appId)) return { user: {}, isCreated: false }; + + const now = new Date().toISOString(); + const dbUserId = convertAppUserIdToUserId({ appId, appUserId }); + + const { + body: { result, get: userFound }, + } = await client.update({ + index: 'users', + type: 'doc', + id: dbUserId, + body: { + doc: { + lastActiveAt: now, + }, + upsert: { + name: generatePseudonym(), + avatarType: AvatarTypes.OpenPeeps, + avatarData: JSON.stringify(generateOpenPeepsAvatar()), + appId, + appUserId, + createdAt: now, + updatedAt: now, + lastActiveAt: now, + }, + _source: true, + }, + }); + + const isCreated = result === 'created'; + const user = processMeta({ ...userFound, _id: dbUserId }); + if (!isCreated && (user.appId !== appId || user.appUserId !== appUserId)) { + const errorMessage = `collision found! ${user.appUserId + } and ${appUserId} both hash to ${dbUserId}`; + console.log(errorMessage); + rollbar.error(`createBackendUserError: ${errorMessage}`); + } + return { + user, + isCreated, + }; +} + +export default { + description: 'Create or update a user for the given appId, appUserId pair', + type: User, + args: {}, + + async resolve(rootValue, _, { appId, userId }) { + const { user } = await createOrUpdateBackendUser({ + appId, + appUserId: userId, + }); + return user; + }, +}; diff --git a/src/graphql/mutations/__fixtures__/CreateOrUpdateUser.js b/src/graphql/mutations/__fixtures__/CreateOrUpdateUser.js new file mode 100644 index 00000000..a951e895 --- /dev/null +++ b/src/graphql/mutations/__fixtures__/CreateOrUpdateUser.js @@ -0,0 +1,7 @@ +export default { + '/users/doc/6LOqD_QabTT7XXTrsz7ybEa5PLfc0GfXlV578HYPhODPfSWc8': { + name: 'test user 1', + appUserId: 'testUser1', + appId: 'TEST_BACKEND', + }, +}; diff --git a/src/graphql/mutations/__tests__/CreateOrUpdateUser.js b/src/graphql/mutations/__tests__/CreateOrUpdateUser.js new file mode 100644 index 00000000..f0fa0a14 --- /dev/null +++ b/src/graphql/mutations/__tests__/CreateOrUpdateUser.js @@ -0,0 +1,151 @@ +import gql from 'util/GraphQL'; +import { loadFixtures, saveStateForIndices, clearIndices } from 'util/fixtures'; +import client from 'util/client'; +import MockDate from 'mockdate'; +import fixtures from '../__fixtures__/CreateOrUpdateUser'; +import rollbar from 'rollbarInstance'; +import { convertAppUserIdToUserId } from 'graphql/models/User'; + + +jest.mock('../../models/User', () => { + const UserModel = jest.requireActual('../../models/User'); + return { + ...UserModel, + __esModule: true, + generatePseudonym: jest.fn().mockReturnValue('Friendly Neighborhood Spider Man'), + generateOpenPeepsAvatar: jest.fn().mockReturnValue({ 'accessory': 'mask' }), + convertAppUserIdToUserId: jest.spyOn(UserModel, 'convertAppUserIdToUserId') + } +}); + +jest.mock('../../../rollbarInstance', () => ({ + __esModule: true, + default: { error: jest.fn() } +})) + +let dbStates = {}; + +describe('CreateOrUpdateUser', () => { + beforeAll(async () => { + // dbStates = await saveStateForIndices(['users']); + await loadFixtures(fixtures); + }); + + afterAll(async () => { + await client.delete({ + index: 'users', + type: 'doc', + id: convertAppUserIdToUserId({ + appUserId: 'testUser2', + appId: 'TEST_BACKEND', + }), + }); + await client.delete({ + index: 'users', + type: 'doc', + id: convertAppUserIdToUserId({ + appUserId: 'testUser1', + appId: 'TEST_BACKEND', + }), + }); + + //await clearIndices(['users']); + // restore db states to prevent affecting other tests + // await loadFixtures(dbStates); + }); + + it('creates backend user if not existed', async () => { + MockDate.set(1602288000000); + const userId = 'testUser2'; + const appId = 'TEST_BACKEND'; + + const { data, errors } = await gql` + mutation { + CreateOrUpdateUser { + id + name + createdAt + updatedAt + } + } + `({}, { userId, appId }); + + expect(errors).toBeUndefined(); + expect(data).toMatchSnapshot(); + + const id = convertAppUserIdToUserId({ appUserId: userId, appId }); + + const { + body: { _source: user }, + } = await client.get({ + index: 'users', + type: 'doc', + id, + }); + expect(user).toMatchSnapshot(); + + MockDate.reset(); + }); + + it("updates backend users' last active time if user already existed", async () => { + MockDate.set(1602291600000); + + const userId = 'testUser1'; + const appId = 'TEST_BACKEND'; + + const { data, errors } = await gql` + mutation { + CreateOrUpdateUser { + id + name + createdAt + updatedAt + } + } + `({}, { userId, appId }); + + expect(errors).toBeUndefined(); + expect(data).toMatchSnapshot(); + + const id = convertAppUserIdToUserId({ appUserId: userId, appId }); + const { + body: { _source: user }, + } = await client.get({ + index: 'users', + type: 'doc', + id, + }); + expect(user).toMatchSnapshot(); + }); + + it('logs error if collision occurs', async () => { + MockDate.set(1602291600000); + + const userId = 'testUser2'; + const appId = 'TEST_BACKEND'; + const id = convertAppUserIdToUserId({ appUserId: 'testUser1', appId }); + + convertAppUserIdToUserId.mockReturnValueOnce(id); + const { data, errors } = await gql` + mutation { + CreateOrUpdateUser { + id + name + } + } + `({}, { userId, appId }); + + expect(errors).toBeUndefined(); + expect(data).toMatchSnapshot(); + + const { + body: { _source: user }, + } = await client.get({ + index: 'users', + type: 'doc', + id, + }); + expect(user).toMatchSnapshot(); + expect(rollbar.error.mock.calls).toMatchSnapshot(); + }); +}); diff --git a/src/graphql/mutations/__tests__/__snapshots__/CreateOrUpdateUser.js.snap b/src/graphql/mutations/__tests__/__snapshots__/CreateOrUpdateUser.js.snap new file mode 100644 index 00000000..b7741182 --- /dev/null +++ b/src/graphql/mutations/__tests__/__snapshots__/CreateOrUpdateUser.js.snap @@ -0,0 +1,71 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CreateOrUpdateUser creates backend user if not existed 1`] = ` +Object { + "CreateOrUpdateUser": Object { + "createdAt": "2020-10-10T00:00:00.000Z", + "id": "6LOqD_3gpe4ZVaxRvemf7KNTfm6y3WNBu1hbs-5MRdSWiWVss", + "name": "Friendly Neighborhood Spider Man", + "updatedAt": "2020-10-10T00:00:00.000Z", + }, +} +`; + +exports[`CreateOrUpdateUser creates backend user if not existed 2`] = ` +Object { + "appId": "TEST_BACKEND", + "appUserId": "testUser2", + "avatarData": "{\\"accessory\\":\\"mask\\"}", + "avatarType": "OpenPeeps", + "createdAt": "2020-10-10T00:00:00.000Z", + "lastActiveAt": "2020-10-10T00:00:00.000Z", + "name": "Friendly Neighborhood Spider Man", + "updatedAt": "2020-10-10T00:00:00.000Z", +} +`; + +exports[`CreateOrUpdateUser logs error if collision occurs 1`] = ` +Object { + "CreateOrUpdateUser": Object { + "id": "6LOqD_QabTT7XXTrsz7ybEa5PLfc0GfXlV578HYPhODPfSWc8", + "name": "test user 1", + }, +} +`; + +exports[`CreateOrUpdateUser logs error if collision occurs 2`] = ` +Object { + "appId": "TEST_BACKEND", + "appUserId": "testUser1", + "lastActiveAt": "2020-10-10T01:00:00.000Z", + "name": "test user 1", +} +`; + +exports[`CreateOrUpdateUser logs error if collision occurs 3`] = ` +Array [ + Array [ + "createBackendUserError: collision found! testUser1 and testUser2 both hash to 6LOqD_QabTT7XXTrsz7ybEa5PLfc0GfXlV578HYPhODPfSWc8", + ], +] +`; + +exports[`CreateOrUpdateUser updates backend users' last active time if user already existed 1`] = ` +Object { + "CreateOrUpdateUser": Object { + "createdAt": null, + "id": "6LOqD_QabTT7XXTrsz7ybEa5PLfc0GfXlV578HYPhODPfSWc8", + "name": "test user 1", + "updatedAt": null, + }, +} +`; + +exports[`CreateOrUpdateUser updates backend users' last active time if user already existed 2`] = ` +Object { + "appId": "TEST_BACKEND", + "appUserId": "testUser1", + "lastActiveAt": "2020-10-10T01:00:00.000Z", + "name": "test user 1", +} +`; diff --git a/src/graphql/schema.js b/src/graphql/schema.js index ea31661d..53491436 100644 --- a/src/graphql/schema.js +++ b/src/graphql/schema.js @@ -20,6 +20,7 @@ import CreateOrUpdateArticleReplyFeedback from './mutations/CreateOrUpdateArticl import CreateOrUpdateReplyRequestFeedback from './mutations/CreateOrUpdateReplyRequestFeedback'; import CreateOrUpdateArticleCategoryFeedback from './mutations/CreateOrUpdateArticleCategoryFeedback'; import CreateOrUpdateReplyRequest from './mutations/CreateOrUpdateReplyRequest'; +import CreateOrUpdateUser from './mutations/CreateOrUpdateUser'; import UpdateArticleReplyStatus from './mutations/UpdateArticleReplyStatus'; import UpdateArticleCategoryStatus from './mutations/UpdateArticleCategoryStatus'; import UpdateUser from './mutations/UpdateUser'; @@ -54,6 +55,7 @@ export default new GraphQLSchema({ CreateOrUpdateArticleReplyFeedback, CreateOrUpdateArticleCategoryFeedback, CreateOrUpdateReplyRequestFeedback, + CreateOrUpdateUser, UpdateArticleReplyStatus, UpdateArticleCategoryStatus, UpdateUser, diff --git a/src/index.js b/src/index.js index 7e0d4899..cc2e6f84 100644 --- a/src/index.js +++ b/src/index.js @@ -15,9 +15,10 @@ import schema from './graphql/schema'; import DataLoaders from './graphql/dataLoaders'; import { AUTH_ERROR_MSG } from './graphql/util'; import CookieStore from './CookieStore'; - +import { isBackendApp } from 'graphql/models/User'; import { loginRouter, authRouter } from './auth'; import rollbar from './rollbarInstance'; +import { createOrUpdateBackendUser } from './graphql/mutations/CreateOrUpdateUser'; const app = new Koa(); const router = Router(); @@ -82,18 +83,26 @@ const apolloServer = new ApolloServer({ schema, introspection: true, // Allow introspection in production as well playground: true, - context: ({ ctx }) => ({ - loaders: new DataLoaders(), // new loaders per request - user: ctx.state.user, - - // userId-appId pair - // - userId: - ctx.appId === 'WEBSITE' || ctx.appId === 'DEVELOPMENT_FRONTEND' - ? (ctx.state.user || {}).id - : ctx.query.userId, - appId: ctx.appId, - }), + context: ({ ctx }) => { + let userId; + if (isBackendApp(ctx.appId)) { + userId = createOrUpdateBackendUser({ + appUserId: ctx.query.userId, + appId: ctx.appId, + }).userId; + } else { + userId = ctx.state.user?.id; + } + return { + loaders: new DataLoaders(), // new loaders per request + user: ctx.state.user, + + // userId-appId pair + // + userId, + appId: ctx.appId, + }; + }, formatError(err) { // make web clients know they should login // diff --git a/src/scripts/__tests__/fetchStatsFromGA.js b/src/scripts/__tests__/fetchStatsFromGA.js index 032eb237..35cc8ccf 100644 --- a/src/scripts/__tests__/fetchStatsFromGA.js +++ b/src/scripts/__tests__/fetchStatsFromGA.js @@ -363,7 +363,6 @@ describe('fetchStatsFromGA', () => { afterEach(() => { upsertDocStatsMock.mockReset(); }); - it('should call bulkUpdates with right params', async () => { await fetchStatsFromGA.processReport( 'WEB', diff --git a/src/scripts/migrations/__tests__/createBackendUsers.js b/src/scripts/migrations/__tests__/createBackendUsers.js index 9976ce8d..68c51e4d 100644 --- a/src/scripts/migrations/__tests__/createBackendUsers.js +++ b/src/scripts/migrations/__tests__/createBackendUsers.js @@ -3,6 +3,7 @@ import client from 'util/client'; import CreateBackendUsers from '../createBackendUsers'; import fixtures from '../__fixtures__/createBackendUsers'; import { sortBy } from 'lodash'; +jest.setTimeout(50000); const checkAllDocsForIndex = async index => { let res = {}; diff --git a/src/scripts/migrations/createBackendUsers.js b/src/scripts/migrations/createBackendUsers.js index 3b08afef..f5d094df 100644 --- a/src/scripts/migrations/createBackendUsers.js +++ b/src/scripts/migrations/createBackendUsers.js @@ -201,7 +201,7 @@ export default class CreateBackendUsers { if (error) { logError(error); } else if (isBackendApp(appId)) { - const dbUserId = convertAppUserIdToUserId(appId, userId); + const dbUserId = convertAppUserIdToUserId({ appId, appUserId: userId }); const appUserId = get(this.reversedUserIdMap, [dbUserId, 1]); // if the hashed id already exists, check for collision if (appUserId !== undefined) { From 3fd19e04df2f5e7ced03088836746674dc397d1b Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Mon, 12 Oct 2020 18:55:03 +0800 Subject: [PATCH 02/20] fix a flaky test --- src/graphql/queries/__tests__/ListReplies.js | 22 +++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/graphql/queries/__tests__/ListReplies.js b/src/graphql/queries/__tests__/ListReplies.js index de570d25..5bb02230 100644 --- a/src/graphql/queries/__tests__/ListReplies.js +++ b/src/graphql/queries/__tests__/ListReplies.js @@ -1,10 +1,20 @@ +import { loadFixtures, clearIndices, saveStateForIndices } from 'util/fixtures'; import gql from 'util/GraphQL'; -import { loadFixtures, unloadFixtures } from 'util/fixtures'; import { getCursor } from 'graphql/util'; import fixtures from '../__fixtures__/ListReplies'; +const indices = [ + 'replies', + 'urls', +]; +let dbStates; describe('ListReplies', () => { - beforeAll(() => loadFixtures(fixtures)); + beforeAll(async () => { + // storing the current db states to restore to after the test is completed + dbStates = await saveStateForIndices(indices); + await clearIndices(indices); + await loadFixtures(fixtures); + }) it('lists all replies', async () => { expect( @@ -383,5 +393,11 @@ describe('ListReplies', () => { ).toMatchSnapshot(); }); - afterAll(() => unloadFixtures(fixtures)); + + afterAll(async () => { + await clearIndices(indices); + // restore db states to prevent affecting other tests + await loadFixtures(dbStates); + }); + }); From 8486866ccab0ac1bbb5296bbb1d03b1c93497817 Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Mon, 12 Oct 2020 19:01:06 +0800 Subject: [PATCH 03/20] lint --- src/graphql/models/User.js | 4 +- src/graphql/mutations/CreateOrUpdateUser.js | 5 ++- .../mutations/__tests__/CreateOrUpdateUser.js | 40 ++++++------------- src/graphql/queries/__tests__/ListReplies.js | 9 +---- 4 files changed, 19 insertions(+), 39 deletions(-) diff --git a/src/graphql/models/User.js b/src/graphql/models/User.js index a41c8ad9..e4d6e602 100644 --- a/src/graphql/models/User.js +++ b/src/graphql/models/User.js @@ -210,9 +210,9 @@ export const userFieldResolver = async ( args, { loaders, ...context } ) => { - // If the root document is created by website users or if the userId is already converted to db userId, + // If the root document is created by website users or if the userId is already converted to db userId, // we can resolve user from userId. - // + // if (!isBackendApp(appId)) return await loaders.docLoader.load({ index: 'users', id: userId }); diff --git a/src/graphql/mutations/CreateOrUpdateUser.js b/src/graphql/mutations/CreateOrUpdateUser.js index bf71dfcf..9146f497 100644 --- a/src/graphql/mutations/CreateOrUpdateUser.js +++ b/src/graphql/mutations/CreateOrUpdateUser.js @@ -52,8 +52,9 @@ export async function createOrUpdateBackendUser({ appUserId, appId }) { const isCreated = result === 'created'; const user = processMeta({ ...userFound, _id: dbUserId }); if (!isCreated && (user.appId !== appId || user.appUserId !== appUserId)) { - const errorMessage = `collision found! ${user.appUserId - } and ${appUserId} both hash to ${dbUserId}`; + const errorMessage = `collision found! ${ + user.appUserId + } and ${appUserId} both hash to ${dbUserId}`; console.log(errorMessage); rollbar.error(`createBackendUserError: ${errorMessage}`); } diff --git a/src/graphql/mutations/__tests__/CreateOrUpdateUser.js b/src/graphql/mutations/__tests__/CreateOrUpdateUser.js index f0fa0a14..bd1e9c20 100644 --- a/src/graphql/mutations/__tests__/CreateOrUpdateUser.js +++ b/src/graphql/mutations/__tests__/CreateOrUpdateUser.js @@ -6,52 +6,36 @@ import fixtures from '../__fixtures__/CreateOrUpdateUser'; import rollbar from 'rollbarInstance'; import { convertAppUserIdToUserId } from 'graphql/models/User'; - jest.mock('../../models/User', () => { const UserModel = jest.requireActual('../../models/User'); return { ...UserModel, __esModule: true, - generatePseudonym: jest.fn().mockReturnValue('Friendly Neighborhood Spider Man'), - generateOpenPeepsAvatar: jest.fn().mockReturnValue({ 'accessory': 'mask' }), - convertAppUserIdToUserId: jest.spyOn(UserModel, 'convertAppUserIdToUserId') - } + generatePseudonym: jest + .fn() + .mockReturnValue('Friendly Neighborhood Spider Man'), + generateOpenPeepsAvatar: jest.fn().mockReturnValue({ accessory: 'mask' }), + convertAppUserIdToUserId: jest.spyOn(UserModel, 'convertAppUserIdToUserId'), + }; }); jest.mock('../../../rollbarInstance', () => ({ __esModule: true, - default: { error: jest.fn() } -})) + default: { error: jest.fn() }, +})); let dbStates = {}; describe('CreateOrUpdateUser', () => { beforeAll(async () => { - // dbStates = await saveStateForIndices(['users']); + dbStates = await saveStateForIndices(['users']); await loadFixtures(fixtures); }); afterAll(async () => { - await client.delete({ - index: 'users', - type: 'doc', - id: convertAppUserIdToUserId({ - appUserId: 'testUser2', - appId: 'TEST_BACKEND', - }), - }); - await client.delete({ - index: 'users', - type: 'doc', - id: convertAppUserIdToUserId({ - appUserId: 'testUser1', - appId: 'TEST_BACKEND', - }), - }); - - //await clearIndices(['users']); + await clearIndices(['users']); // restore db states to prevent affecting other tests - // await loadFixtures(dbStates); + await loadFixtures(dbStates); }); it('creates backend user if not existed', async () => { @@ -65,7 +49,7 @@ describe('CreateOrUpdateUser', () => { id name createdAt - updatedAt + updatedAt } } `({}, { userId, appId }); diff --git a/src/graphql/queries/__tests__/ListReplies.js b/src/graphql/queries/__tests__/ListReplies.js index 5bb02230..e390ea48 100644 --- a/src/graphql/queries/__tests__/ListReplies.js +++ b/src/graphql/queries/__tests__/ListReplies.js @@ -3,10 +3,7 @@ import gql from 'util/GraphQL'; import { getCursor } from 'graphql/util'; import fixtures from '../__fixtures__/ListReplies'; -const indices = [ - 'replies', - 'urls', -]; +const indices = ['replies', 'urls']; let dbStates; describe('ListReplies', () => { beforeAll(async () => { @@ -14,7 +11,7 @@ describe('ListReplies', () => { dbStates = await saveStateForIndices(indices); await clearIndices(indices); await loadFixtures(fixtures); - }) + }); it('lists all replies', async () => { expect( @@ -393,11 +390,9 @@ describe('ListReplies', () => { ).toMatchSnapshot(); }); - afterAll(async () => { await clearIndices(indices); // restore db states to prevent affecting other tests await loadFixtures(dbStates); }); - }); From c258fcadcc84ca75b646779bc97c78581a3fee36 Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Tue, 13 Oct 2020 16:16:51 +0800 Subject: [PATCH 04/20] unit test --- src/__tests__/auth.js | 8 +- src/graphql/models/User.js | 36 +++++---- src/graphql/models/__fixtures__/User.js | 12 +++ src/graphql/models/__tests__/User.js | 72 ++++++++++++++++- .../__tests__/__snapshots__/User.js.snap | 77 +++++++++++++++++++ 5 files changed, 189 insertions(+), 16 deletions(-) create mode 100644 src/graphql/models/__fixtures__/User.js create mode 100644 src/graphql/models/__tests__/__snapshots__/User.js.snap diff --git a/src/__tests__/auth.js b/src/__tests__/auth.js index 525e97de..9e008dcf 100644 --- a/src/__tests__/auth.js +++ b/src/__tests__/auth.js @@ -2,12 +2,16 @@ import MockDate from 'mockdate'; import { loadFixtures, unloadFixtures } from 'util/fixtures'; import fixtures from '../__fixtures__/auth'; import { verifyProfile } from '../auth'; +import client from 'util/client'; const FIXED_DATE = 612921600000; describe('verifyProfile', () => { - beforeEach(() => loadFixtures(fixtures)); - afterEach(() => unloadFixtures(fixtures)); + beforeAll(() => loadFixtures(fixtures)); + afterAll(async () => { + await unloadFixtures(fixtures); + await client.delete({ index: 'users', type: 'doc', id: 'another-fb-id' }); + }); it('authenticates user via profile ID', async () => { const passportProfile = { diff --git a/src/graphql/models/User.js b/src/graphql/models/User.js index e4d6e602..ee2f1d63 100644 --- a/src/graphql/models/User.js +++ b/src/graphql/models/User.js @@ -66,6 +66,18 @@ export const generateOpenPeepsAvatar = () => { }; }; +/** + * check if the userId for a backend user is the user id in db or it is the app user Id. + */ +export const isDBUserId = ({ appId, userId }) => { + return ( + appId && + userId && + userId.length === 49 && + userId.substr(0, 6) === `${encodeAppId(appId)}_` + ); +}; + export const encodeAppId = appId => crypto .createHash('md5') @@ -133,7 +145,7 @@ const User = new GraphQLObjectType({ email: currentUserOnlyField(GraphQLString), name: { type: GraphQLString }, avatarUrl: avatarResolver(), - // avatarData: { type: GraphQLString }, + avatarData: { type: GraphQLString }, facebookId: currentUserOnlyField(GraphQLString), githubId: currentUserOnlyField(GraphQLString), @@ -199,7 +211,7 @@ const User = new GraphQLObjectType({ }, createdAt: { type: GraphQLString }, updatedAt: { type: GraphQLString }, - // lastActiveAt: { type: GraphQLString }, + lastActiveAt: { type: GraphQLString }, }), }); @@ -213,20 +225,18 @@ export const userFieldResolver = async ( // If the root document is created by website users or if the userId is already converted to db userId, // we can resolve user from userId. // - if (!isBackendApp(appId)) - return await loaders.docLoader.load({ index: 'users', id: userId }); - - /* - if (userId && userId.substr(0, 6) === `${encodeAppId(appId)}_`) { - return await loaders.docLoader.load({ index: 'users', id: userId }); + if (userId && appId) { + const id = + !isBackendApp(appId) || isDBUserId({ appId, userId }) + ? userId + : convertAppUserIdToUserId({ appId, appUserId: userId }); + const user = await loaders.docLoader.load({ index: 'users', id }); + if (user) return user; } - - const user = await loaders.docLoader.load({ index: 'users', id: convertAppUserIdToUserId({ appId, appUserId: userId }) }); - if (user) return user; - */ + /* TODO: some unit tests are depending on this code block, need to clean up those tests and then + remove the following lines. */ - // Todo: some unit tests are depending on this code block, need to clean up those tests and then remove the following lines. // If the user comes from the same client as the root document, return the user id. // if (context.appId === appId) return { id: userId }; diff --git a/src/graphql/models/__fixtures__/User.js b/src/graphql/models/__fixtures__/User.js new file mode 100644 index 00000000..b5975682 --- /dev/null +++ b/src/graphql/models/__fixtures__/User.js @@ -0,0 +1,12 @@ +export default { + '/users/doc/6LOqD_z5A8BwUr4gh1P2gw_2zFU3IIrSchTSl-vemod7CChMU': { + name: 'test user 1', + appUserId: 'userTest1', + appId: 'TEST_BACKEND', + }, + + '/users/doc/userTest2': { + name: 'test user 2', + appId: 'WEBSITE', + }, +}; diff --git a/src/graphql/models/__tests__/User.js b/src/graphql/models/__tests__/User.js index 35c94e18..9e3eb9c7 100644 --- a/src/graphql/models/__tests__/User.js +++ b/src/graphql/models/__tests__/User.js @@ -1,5 +1,13 @@ +import fixtures from '../__fixtures__/User'; import { random, sample } from 'lodash'; -import { generatePseudonym, generateOpenPeepsAvatar } from '../User'; +import { + generatePseudonym, + generateOpenPeepsAvatar, + userFieldResolver, +} from '../User'; +import { loadFixtures, unloadFixtures } from 'util/fixtures'; +import DataLoaders from '../../dataLoaders'; + jest.mock('lodash', () => ({ random: jest.fn(), @@ -7,6 +15,68 @@ jest.mock('lodash', () => ({ })); describe('User model', () => { + it('userFieldResolver returns the right user', async () => { + await loadFixtures(fixtures); + + const loaders = new DataLoaders(); + expect( + await userFieldResolver( + { + appId: 'TEST_BACKEND', + userId: 'userTest1', + }, + {}, + { loaders } + ) + ).toMatchSnapshot(); + + expect( + await userFieldResolver( + { + userId: '6LOqD_z5A8BwUr4gh1P2gw_2zFU3IIrSchTSl-vemod7CChMU', + appId: 'TEST_BACKEND', + }, + {}, + { loaders } + ) + ).toMatchSnapshot(); + + expect( + await userFieldResolver( + { + userId: 'userTest2', + appId: 'WEBSITE', + }, + {}, + { loaders } + ) + ).toMatchSnapshot(); + + expect( + await userFieldResolver( + { + userId: 'userTest3', + appId: 'WEBSITE', + }, + {}, + { loaders } + ) + ).toBe(null); + + expect( + await userFieldResolver( + { + userId: 'userTest3', + appId: 'TEST_BACKEND', + }, + {}, + { loaders, appId: 'TEST_BACKEND' } + ) + ).toStrictEqual({ id: 'userTest3' }); + + await unloadFixtures(fixtures); + }); + describe('pseudo name and avatar generators', () => { it('should generate pseudo names for user', () => { [ diff --git a/src/graphql/models/__tests__/__snapshots__/User.js.snap b/src/graphql/models/__tests__/__snapshots__/User.js.snap new file mode 100644 index 00000000..3443c840 --- /dev/null +++ b/src/graphql/models/__tests__/__snapshots__/User.js.snap @@ -0,0 +1,77 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`User model userFieldResolver return the right user 1`] = ` +Object { + "_cursor": undefined, + "_score": undefined, + "appId": "TEST_BACKEND", + "appUserId": "userTest1", + "highlight": undefined, + "id": "6LOqD_z5A8BwUr4gh1P2gw_2zFU3IIrSchTSl-vemod7CChMU", + "inner_hits": undefined, + "name": "test user 1", +} +`; + +exports[`User model userFieldResolver return the right user 2`] = ` +Object { + "_cursor": undefined, + "_score": undefined, + "appId": "TEST_BACKEND", + "appUserId": "userTest1", + "highlight": undefined, + "id": "6LOqD_z5A8BwUr4gh1P2gw_2zFU3IIrSchTSl-vemod7CChMU", + "inner_hits": undefined, + "name": "test user 1", +} +`; + +exports[`User model userFieldResolver return the right user 3`] = ` +Object { + "_cursor": undefined, + "_score": undefined, + "appId": "WEBSITE", + "highlight": undefined, + "id": "userTest2", + "inner_hits": undefined, + "name": "test user 2", +} +`; + +exports[`User model userFieldResolver returns the right user 1`] = ` +Object { + "_cursor": undefined, + "_score": undefined, + "appId": "TEST_BACKEND", + "appUserId": "userTest1", + "highlight": undefined, + "id": "6LOqD_z5A8BwUr4gh1P2gw_2zFU3IIrSchTSl-vemod7CChMU", + "inner_hits": undefined, + "name": "test user 1", +} +`; + +exports[`User model userFieldResolver returns the right user 2`] = ` +Object { + "_cursor": undefined, + "_score": undefined, + "appId": "TEST_BACKEND", + "appUserId": "userTest1", + "highlight": undefined, + "id": "6LOqD_z5A8BwUr4gh1P2gw_2zFU3IIrSchTSl-vemod7CChMU", + "inner_hits": undefined, + "name": "test user 1", +} +`; + +exports[`User model userFieldResolver returns the right user 3`] = ` +Object { + "_cursor": undefined, + "_score": undefined, + "appId": "WEBSITE", + "highlight": undefined, + "id": "userTest2", + "inner_hits": undefined, + "name": "test user 2", +} +`; From 9e791a9d8ea1068952ed624c6dd551cba2acee1d Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Tue, 13 Oct 2020 20:18:58 +0800 Subject: [PATCH 05/20] apollo server integration tst --- package-lock.json | 241 ++++++++++++++++++ package.json | 1 + src/__fixtures__/index.js | 7 + src/__tests__/index.js | 120 +++++++++ src/graphql/models/__tests__/User.js | 1 - .../__tests__/__snapshots__/User.js.snap | 38 --- src/graphql/mutations/CreateOrUpdateUser.js | 1 + src/index.js | 20 +- 8 files changed, 380 insertions(+), 49 deletions(-) create mode 100644 src/__fixtures__/index.js create mode 100644 src/__tests__/index.js diff --git a/package-lock.json b/package-lock.json index 430fbf51..851a5ff5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5155,6 +5155,15 @@ "zen-observable-ts": "^0.8.20" } }, + "apollo-reporting-protobuf": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.6.0.tgz", + "integrity": "sha512-AFLQIuO0QhkoCF+41Be/B/YU0C33BZ0opfyXorIjM3MNNiEDSyjZqmUozlB3LqgfhT9mn2IR5RSsA+1b4VovDQ==", + "dev": true, + "requires": { + "@apollo/protobufjs": "^1.0.3" + } + }, "apollo-server-caching": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.5.1.tgz", @@ -5328,6 +5337,208 @@ "apollo-server-types": "^0.3.0" } }, + "apollo-server-testing": { + "version": "2.18.2", + "resolved": "https://registry.npmjs.org/apollo-server-testing/-/apollo-server-testing-2.18.2.tgz", + "integrity": "sha512-4qoJ8AYUKolqV397G7XXRjOCM1ADQB66ReL/B5NxdoROWTvxIOW2m1NOrmvMX1w6AmpSUD5APRM27I7fC3f65g==", + "dev": true, + "requires": { + "apollo-server-core": "^2.18.2" + }, + "dependencies": { + "@apollographql/graphql-playground-html": { + "version": "1.6.26", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.26.tgz", + "integrity": "sha512-XAwXOIab51QyhBxnxySdK3nuMEUohhDsHQ5Rbco/V1vjlP75zZ0ZLHD9dTpXTN8uxKxopb2lUvJTq+M4g2Q0HQ==", + "dev": true, + "requires": { + "xss": "^1.0.6" + } + }, + "@types/node-fetch": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz", + "integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==", + "dev": true, + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "@types/ws": { + "version": "7.2.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.2.7.tgz", + "integrity": "sha512-UUFC/xxqFLP17hTva8/lVT0SybLUrfSD9c+iapKb0fEiC8uoDbA+xuZ3pAN603eW+bY8ebSMLm9jXdIPnD0ZgA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "apollo-cache-control": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.11.3.tgz", + "integrity": "sha512-21GCeC9AIIa22uD0Vtqn/N0D5kOB4rY/Pa9aQhxVeLN+4f8Eu4nmteXhFypUD0LL1/58dmm8lS5embsfoIGjEA==", + "dev": true, + "requires": { + "apollo-server-env": "^2.4.5", + "apollo-server-plugin-base": "^0.10.1" + } + }, + "apollo-datasource": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.7.2.tgz", + "integrity": "sha512-ibnW+s4BMp4K2AgzLEtvzkjg7dJgCaw9M5b5N0YKNmeRZRnl/I/qBTQae648FsRKgMwTbRQIvBhQ0URUFAqFOw==", + "dev": true, + "requires": { + "apollo-server-caching": "^0.5.2", + "apollo-server-env": "^2.4.5" + } + }, + "apollo-env": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/apollo-env/-/apollo-env-0.6.5.tgz", + "integrity": "sha512-jeBUVsGymeTHYWp3me0R2CZRZrFeuSZeICZHCeRflHTfnQtlmbSXdy5E0pOyRM9CU4JfQkKDC98S1YglQj7Bzg==", + "dev": true, + "requires": { + "@types/node-fetch": "2.5.7", + "core-js": "^3.0.1", + "node-fetch": "^2.2.0", + "sha.js": "^2.4.11" + } + }, + "apollo-graphql": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.6.0.tgz", + "integrity": "sha512-BxTf5LOQe649e9BNTPdyCGItVv4Ll8wZ2BKnmiYpRAocYEXAVrQPWuSr3dO4iipqAU8X0gvle/Xu9mSqg5b7Qg==", + "dev": true, + "requires": { + "apollo-env": "^0.6.5", + "lodash.sortby": "^4.7.0" + } + }, + "apollo-server-caching": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.5.2.tgz", + "integrity": "sha512-HUcP3TlgRsuGgeTOn8QMbkdx0hLPXyEJehZIPrcof0ATz7j7aTPA4at7gaiFHCo8gk07DaWYGB3PFgjboXRcWQ==", + "dev": true, + "requires": { + "lru-cache": "^5.0.0" + } + }, + "apollo-server-core": { + "version": "2.18.2", + "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.18.2.tgz", + "integrity": "sha512-phz57BFBukMa3Ta7ZVW7pj1pdUne9KYLbcBdEcITr+I0+nbhy+YM8gcgpOnjrokWYiEZgIe52XeM3m4BMLw5dg==", + "dev": true, + "requires": { + "@apollographql/apollo-tools": "^0.4.3", + "@apollographql/graphql-playground-html": "1.6.26", + "@types/graphql-upload": "^8.0.0", + "@types/ws": "^7.0.0", + "apollo-cache-control": "^0.11.3", + "apollo-datasource": "^0.7.2", + "apollo-graphql": "^0.6.0", + "apollo-reporting-protobuf": "^0.6.0", + "apollo-server-caching": "^0.5.2", + "apollo-server-env": "^2.4.5", + "apollo-server-errors": "^2.4.2", + "apollo-server-plugin-base": "^0.10.1", + "apollo-server-types": "^0.6.0", + "apollo-tracing": "^0.11.4", + "async-retry": "^1.2.1", + "fast-json-stable-stringify": "^2.0.0", + "graphql-extensions": "^0.12.5", + "graphql-tag": "^2.9.2", + "graphql-tools": "^4.0.0", + "graphql-upload": "^8.0.2", + "loglevel": "^1.6.7", + "lru-cache": "^5.0.0", + "sha.js": "^2.4.11", + "subscriptions-transport-ws": "^0.9.11", + "uuid": "^8.0.0", + "ws": "^6.0.0" + } + }, + "apollo-server-env": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.4.5.tgz", + "integrity": "sha512-nfNhmGPzbq3xCEWT8eRpoHXIPNcNy3QcEoBlzVMjeglrBGryLG2LXwBSPnVmTRRrzUYugX0ULBtgE3rBFNoUgA==", + "dev": true, + "requires": { + "node-fetch": "^2.1.2", + "util.promisify": "^1.0.0" + } + }, + "apollo-server-errors": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.4.2.tgz", + "integrity": "sha512-FeGxW3Batn6sUtX3OVVUm7o56EgjxDlmgpTLNyWcLb0j6P8mw9oLNyAm3B+deHA4KNdNHO5BmHS2g1SJYjqPCQ==", + "dev": true + }, + "apollo-server-plugin-base": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-0.10.1.tgz", + "integrity": "sha512-XChCBDNyfByWqVXptsjPwrwrCj5cxMmNbchZZi8KXjtJ0hN2C/9BMNlInJd6bVGXvUbkRJYUakfKCfO5dZmwIg==", + "dev": true, + "requires": { + "apollo-server-types": "^0.6.0" + } + }, + "apollo-server-types": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-0.6.0.tgz", + "integrity": "sha512-usqXaz81bHxD2IZvKEQNnLpSbf2Z/BmobXZAjEefJEQv1ItNn+lJNUmSSEfGejHvHlg2A7WuAJKJWyDWcJrNnA==", + "dev": true, + "requires": { + "apollo-reporting-protobuf": "^0.6.0", + "apollo-server-caching": "^0.5.2", + "apollo-server-env": "^2.4.5" + } + }, + "apollo-tracing": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.11.4.tgz", + "integrity": "sha512-zBu/SwQlXfbdpcKLzWARGVjrEkIZUW3W9Mb4CCIzv07HbBQ8IQpmf9w7HIJJefC7rBiBJYg6JBGyuro3N2lxCA==", + "dev": true, + "requires": { + "apollo-server-env": "^2.4.5", + "apollo-server-plugin-base": "^0.10.1" + } + }, + "core-js": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==", + "dev": true + }, + "graphql-extensions": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.12.5.tgz", + "integrity": "sha512-mGyGaktGpK3TVBtM0ZoyPX6Xk0mN9GYX9DRyFzDU4k4A2w93nLX7Ebcp+9/O5nHRmgrc0WziYYSmoWq2WNIoUQ==", + "dev": true, + "requires": { + "@apollographql/apollo-tools": "^0.4.3", + "apollo-server-env": "^2.4.5", + "apollo-server-types": "^0.6.0" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "uuid": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==", + "dev": true + } + } + }, "apollo-server-types": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-0.3.0.tgz", @@ -6611,6 +6822,12 @@ "which": "^1.2.9" } }, + "cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4=", + "dev": true + }, "cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", @@ -12017,6 +12234,12 @@ "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==" }, + "loglevel": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.0.tgz", + "integrity": "sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ==", + "dev": true + }, "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -15857,6 +16080,24 @@ "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=" }, + "xss": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.8.tgz", + "integrity": "sha512-3MgPdaXV8rfQ/pNn16Eio6VXYPTkqwa0vc7GkiymmY/DqR1SE/7VPAAVZz1GJsJFrllMYO3RHfEaiUGjab6TNw==", + "dev": true, + "requires": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, "xtraverse": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/xtraverse/-/xtraverse-0.1.0.tgz", diff --git a/package.json b/package.json index b70cd9e9..6f921593 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@babel/plugin-syntax-import-meta": "^7.8.0", "@babel/polyfill": "^7.8.0", "@babel/preset-env": "^7.8.0", + "apollo-server-testing": "^2.18.2", "babel-eslint": "^10.0.3", "babel-jest": "^26.0.0", "babel-plugin-module-resolver": "^4.0.0", diff --git a/src/__fixtures__/index.js b/src/__fixtures__/index.js new file mode 100644 index 00000000..f5848f88 --- /dev/null +++ b/src/__fixtures__/index.js @@ -0,0 +1,7 @@ +export default { + '/users/doc/6LOqD_3gpe4ZVaxRvemf7KNTfm6y3WNBu1hbs-5MRdSWiWVss': { + name: 'test user 2', + appUserId: 'testUser2', + appId: 'TEST_BACKEND', + }, +}; diff --git a/src/__tests__/index.js b/src/__tests__/index.js new file mode 100644 index 00000000..1a01726e --- /dev/null +++ b/src/__tests__/index.js @@ -0,0 +1,120 @@ +import { loadFixtures, unloadFixtures } from 'util/fixtures'; +import client from 'util/client'; +import { apolloServer } from '../index'; +import fixtures from '../__fixtures__/index'; +import { createTestClient } from 'apollo-server-testing'; + +jest.mock('koa', () => { + const Koa = jest.requireActual('koa'); + Koa.prototype.listen = () => null; + return { + default: Koa, + __esModule: true, + }; +}); + +describe('apolloServer', () => { + const actualGraphQLServerOptions = apolloServer.graphQLServerOptions; + const mockGraphQLServerOptions = ctx => async () => + actualGraphQLServerOptions.call(apolloServer, { ctx }); + + beforeAll(async () => { + apolloServer.app; + await loadFixtures(fixtures); + }); + afterAll(async () => { + await unloadFixtures(fixtures); + await client.delete({ + index: 'users', + type: 'doc', + id: '6LOqD_gsUWLlGviSA4KFdKpsNncQfTYeueOl-DGx9fL6zCNeA', + }); + }); + + it('resolves current web user', async () => { + const appId = 'WEBSITE'; + const userId = 'testUser1'; + + apolloServer.graphQLServerOptions = mockGraphQLServerOptions({ + appId, + userId, + state: { user: { id: userId } }, + query: { userId }, + }); + + const testClient = createTestClient(apolloServer); + const { + data: { GetUser: res }, + errors, + } = await testClient.query({ + query: `{ + GetUser { + id + name + } + }`, + }); + expect(errors).toBeUndefined(); + expect(res).toMatchObject({ + id: 'testUser1', + }); + }); + + it('resolves current backend user', async () => { + const appId = 'TEST_BACKEND'; + const userId = 'testUser2'; + + apolloServer.graphQLServerOptions = mockGraphQLServerOptions({ + appId, + userId, + state: {}, + query: { userId }, + }); + const testClient = createTestClient(apolloServer); + const { + data: { GetUser: res }, + errors, + } = await testClient.query({ + query: `{ + GetUser { + id + name + } + }`, + }); + expect(errors).toBeUndefined(); + expect(res).toMatchObject({ + id: '6LOqD_3gpe4ZVaxRvemf7KNTfm6y3WNBu1hbs-5MRdSWiWVss', + name: 'test user 2', + }); + }); + + it('creates new backend user if not existed', async () => { + const appId = 'TEST_BACKEND'; + const userId = 'testUser3'; + + apolloServer.graphQLServerOptions = mockGraphQLServerOptions({ + appId, + userId, + state: {}, + query: { userId }, + }); + const testClient = createTestClient(apolloServer); + const { + data: { GetUser: res }, + errors, + } = await testClient.query({ + query: `{ + GetUser { + id + name + } + }`, + }); + expect(errors).toBeUndefined(); + expect(res).toMatchObject({ + id: '6LOqD_gsUWLlGviSA4KFdKpsNncQfTYeueOl-DGx9fL6zCNeA', + name: expect.any(String), + }); + }); +}); diff --git a/src/graphql/models/__tests__/User.js b/src/graphql/models/__tests__/User.js index 9e3eb9c7..4bf71094 100644 --- a/src/graphql/models/__tests__/User.js +++ b/src/graphql/models/__tests__/User.js @@ -8,7 +8,6 @@ import { import { loadFixtures, unloadFixtures } from 'util/fixtures'; import DataLoaders from '../../dataLoaders'; - jest.mock('lodash', () => ({ random: jest.fn(), sample: jest.fn(), diff --git a/src/graphql/models/__tests__/__snapshots__/User.js.snap b/src/graphql/models/__tests__/__snapshots__/User.js.snap index 3443c840..f2e75a98 100644 --- a/src/graphql/models/__tests__/__snapshots__/User.js.snap +++ b/src/graphql/models/__tests__/__snapshots__/User.js.snap @@ -1,43 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`User model userFieldResolver return the right user 1`] = ` -Object { - "_cursor": undefined, - "_score": undefined, - "appId": "TEST_BACKEND", - "appUserId": "userTest1", - "highlight": undefined, - "id": "6LOqD_z5A8BwUr4gh1P2gw_2zFU3IIrSchTSl-vemod7CChMU", - "inner_hits": undefined, - "name": "test user 1", -} -`; - -exports[`User model userFieldResolver return the right user 2`] = ` -Object { - "_cursor": undefined, - "_score": undefined, - "appId": "TEST_BACKEND", - "appUserId": "userTest1", - "highlight": undefined, - "id": "6LOqD_z5A8BwUr4gh1P2gw_2zFU3IIrSchTSl-vemod7CChMU", - "inner_hits": undefined, - "name": "test user 1", -} -`; - -exports[`User model userFieldResolver return the right user 3`] = ` -Object { - "_cursor": undefined, - "_score": undefined, - "appId": "WEBSITE", - "highlight": undefined, - "id": "userTest2", - "inner_hits": undefined, - "name": "test user 2", -} -`; - exports[`User model userFieldResolver returns the right user 1`] = ` Object { "_cursor": undefined, diff --git a/src/graphql/mutations/CreateOrUpdateUser.js b/src/graphql/mutations/CreateOrUpdateUser.js index 9146f497..e2e1c73b 100644 --- a/src/graphql/mutations/CreateOrUpdateUser.js +++ b/src/graphql/mutations/CreateOrUpdateUser.js @@ -20,6 +20,7 @@ import rollbar from 'rollbarInstance'; */ export async function createOrUpdateBackendUser({ appUserId, appId }) { assertUser({ appId, userId: appUserId }); + if (!isBackendApp(appId)) return { user: {}, isCreated: false }; const now = new Date().toISOString(); diff --git a/src/index.js b/src/index.js index cc2e6f84..6ccd7c56 100644 --- a/src/index.js +++ b/src/index.js @@ -79,27 +79,27 @@ router.get('/', ctx => { app.use(koaStatic(path.join(__dirname, '../static/'))); app.use(router.routes(), router.allowedMethods()); -const apolloServer = new ApolloServer({ +export const apolloServer = new ApolloServer({ schema, introspection: true, // Allow introspection in production as well playground: true, - context: ({ ctx }) => { - let userId; - if (isBackendApp(ctx.appId)) { - userId = createOrUpdateBackendUser({ + context: async ({ ctx }) => { + let user = ctx.state.user || {}; + if (ctx.appId && isBackendApp(ctx.appId)) { + const { user: backendUser } = await createOrUpdateBackendUser({ appUserId: ctx.query.userId, appId: ctx.appId, - }).userId; - } else { - userId = ctx.state.user?.id; + }); + user = { ...user, ...backendUser }; } + return { loaders: new DataLoaders(), // new loaders per request - user: ctx.state.user, + user: user, // userId-appId pair // - userId, + userId: user.id, appId: ctx.appId, }; }, From 19576088abcd94701a874d861eac50ce393cb3b6 Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Sun, 18 Oct 2020 07:20:45 +0800 Subject: [PATCH 06/20] also update web users' last active time --- src/graphql/models/User.js | 30 ++++--- src/graphql/mutations/CreateOrUpdateUser.js | 19 ++--- .../mutations/__tests__/CreateOrUpdateUser.js | 85 ++++++++----------- .../__snapshots__/CreateOrUpdateUser.js.snap | 49 +++++++---- src/graphql/schema.js | 2 - src/index.js | 15 ++-- 6 files changed, 100 insertions(+), 100 deletions(-) diff --git a/src/graphql/models/User.js b/src/graphql/models/User.js index ee2f1d63..ee2d5f15 100644 --- a/src/graphql/models/User.js +++ b/src/graphql/models/User.js @@ -42,6 +42,9 @@ export const AvatarTypes = { export const isBackendApp = appId => appId !== 'WEBSITE' && appId !== 'DEVELOPMENT_FRONTEND'; +// 6 for appId prefix and 43 for 256bit hashed userId with base64 encoding. +const BACKEND_USER_ID_LEN = 6 + 43; + /** * Generates data for open peeps avatar. */ @@ -67,17 +70,23 @@ export const generateOpenPeepsAvatar = () => { }; /** - * check if the userId for a backend user is the user id in db or it is the app user Id. + * Given appId, appUserId pair, returns the id of corresponding user in db. */ -export const isDBUserId = ({ appId, userId }) => { - return ( - appId && - userId && - userId.length === 49 && - userId.substr(0, 6) === `${encodeAppId(appId)}_` - ); +export const getUserId = ({ appId, appUserId }) => { + if (!appId || !isBackendApp(appId) || isDBUserId({ appId, appUserId })) + return appUserId; + else return convertAppUserIdToUserId({ appId, appUserId }); }; +/** + * Check if the userId for a backend user is the user id in db or it is the app user Id. + */ +export const isDBUserId = ({ appId, appUserId }) => + appId && + appUserId && + appUserId.length === BACKEND_USER_ID_LEN && + appUserId.substr(0, 6) === `${encodeAppId(appId)}_`; + export const encodeAppId = appId => crypto .createHash('md5') @@ -226,10 +235,7 @@ export const userFieldResolver = async ( // we can resolve user from userId. // if (userId && appId) { - const id = - !isBackendApp(appId) || isDBUserId({ appId, userId }) - ? userId - : convertAppUserIdToUserId({ appId, appUserId: userId }); + const id = getUserId({ appId, appUserId: userId }); const user = await loaders.docLoader.load({ index: 'users', id }); if (user) return user; } diff --git a/src/graphql/mutations/CreateOrUpdateUser.js b/src/graphql/mutations/CreateOrUpdateUser.js index e2e1c73b..5857a811 100644 --- a/src/graphql/mutations/CreateOrUpdateUser.js +++ b/src/graphql/mutations/CreateOrUpdateUser.js @@ -3,8 +3,7 @@ import User, { generatePseudonym, generateOpenPeepsAvatar, AvatarTypes, - isBackendApp, - convertAppUserIdToUserId, + getUserId, } from 'graphql/models/User'; import client, { processMeta } from 'util/client'; @@ -18,20 +17,17 @@ import rollbar from 'rollbarInstance'; * * @returns {user: User, isCreated: boolean} */ -export async function createOrUpdateBackendUser({ appUserId, appId }) { +export async function createOrUpdateUser({ appUserId, appId }) { assertUser({ appId, userId: appUserId }); - - if (!isBackendApp(appId)) return { user: {}, isCreated: false }; - const now = new Date().toISOString(); - const dbUserId = convertAppUserIdToUserId({ appId, appUserId }); + const userId = getUserId({ appId, appUserId }); const { body: { result, get: userFound }, } = await client.update({ index: 'users', type: 'doc', - id: dbUserId, + id: userId, body: { doc: { lastActiveAt: now, @@ -51,11 +47,12 @@ export async function createOrUpdateBackendUser({ appUserId, appId }) { }); const isCreated = result === 'created'; - const user = processMeta({ ...userFound, _id: dbUserId }); + const user = processMeta({ ...userFound, _id: userId }); + if (!isCreated && (user.appId !== appId || user.appUserId !== appUserId)) { const errorMessage = `collision found! ${ user.appUserId - } and ${appUserId} both hash to ${dbUserId}`; + } and ${appUserId} both hash to ${userId}`; console.log(errorMessage); rollbar.error(`createBackendUserError: ${errorMessage}`); } @@ -71,7 +68,7 @@ export default { args: {}, async resolve(rootValue, _, { appId, userId }) { - const { user } = await createOrUpdateBackendUser({ + const { user } = await createOrUpdateUser({ appId, appUserId: userId, }); diff --git a/src/graphql/mutations/__tests__/CreateOrUpdateUser.js b/src/graphql/mutations/__tests__/CreateOrUpdateUser.js index bd1e9c20..82750a60 100644 --- a/src/graphql/mutations/__tests__/CreateOrUpdateUser.js +++ b/src/graphql/mutations/__tests__/CreateOrUpdateUser.js @@ -1,10 +1,10 @@ -import gql from 'util/GraphQL'; import { loadFixtures, saveStateForIndices, clearIndices } from 'util/fixtures'; import client from 'util/client'; import MockDate from 'mockdate'; import fixtures from '../__fixtures__/CreateOrUpdateUser'; import rollbar from 'rollbarInstance'; -import { convertAppUserIdToUserId } from 'graphql/models/User'; +import { getUserId } from 'graphql/models/User'; +import { createOrUpdateUser } from '../CreateOrUpdateUser'; jest.mock('../../models/User', () => { const UserModel = jest.requireActual('../../models/User'); @@ -15,7 +15,7 @@ jest.mock('../../models/User', () => { .fn() .mockReturnValue('Friendly Neighborhood Spider Man'), generateOpenPeepsAvatar: jest.fn().mockReturnValue({ accessory: 'mask' }), - convertAppUserIdToUserId: jest.spyOn(UserModel, 'convertAppUserIdToUserId'), + getUserId: jest.spyOn(UserModel, 'getUserId'), }; }); @@ -29,6 +29,7 @@ let dbStates = {}; describe('CreateOrUpdateUser', () => { beforeAll(async () => { dbStates = await saveStateForIndices(['users']); + await clearIndices(['users']); await loadFixtures(fixtures); }); @@ -43,30 +44,24 @@ describe('CreateOrUpdateUser', () => { const userId = 'testUser2'; const appId = 'TEST_BACKEND'; - const { data, errors } = await gql` - mutation { - CreateOrUpdateUser { - id - name - createdAt - updatedAt - } - } - `({}, { userId, appId }); + const { user, isCreated } = await createOrUpdateUser({ + appUserId: userId, + appId, + }); - expect(errors).toBeUndefined(); - expect(data).toMatchSnapshot(); + expect(isCreated).toBe(true); + expect(user).toMatchSnapshot(); - const id = convertAppUserIdToUserId({ appUserId: userId, appId }); + const id = getUserId({ appUserId: userId, appId }); const { - body: { _source: user }, + body: { _source: source }, } = await client.get({ index: 'users', type: 'doc', id, }); - expect(user).toMatchSnapshot(); + expect(source).toMatchSnapshot(); MockDate.reset(); }); @@ -77,59 +72,49 @@ describe('CreateOrUpdateUser', () => { const userId = 'testUser1'; const appId = 'TEST_BACKEND'; - const { data, errors } = await gql` - mutation { - CreateOrUpdateUser { - id - name - createdAt - updatedAt - } - } - `({}, { userId, appId }); - - expect(errors).toBeUndefined(); - expect(data).toMatchSnapshot(); - - const id = convertAppUserIdToUserId({ appUserId: userId, appId }); + const { user, isCreated } = await createOrUpdateUser({ + appUserId: userId, + appId, + }); + + expect(isCreated).toBe(false); + expect(user).toMatchSnapshot(); + + const id = getUserId({ appUserId: userId, appId }); const { - body: { _source: user }, + body: { _source: source }, } = await client.get({ index: 'users', type: 'doc', id, }); - expect(user).toMatchSnapshot(); + expect(source).toMatchSnapshot(); }); it('logs error if collision occurs', async () => { MockDate.set(1602291600000); - const userId = 'testUser2'; + const userId = 'testUser3'; const appId = 'TEST_BACKEND'; - const id = convertAppUserIdToUserId({ appUserId: 'testUser1', appId }); + const id = getUserId({ appUserId: 'testUser1', appId }); - convertAppUserIdToUserId.mockReturnValueOnce(id); - const { data, errors } = await gql` - mutation { - CreateOrUpdateUser { - id - name - } - } - `({}, { userId, appId }); + getUserId.mockReturnValueOnce(id); + const { user, isCreated } = await createOrUpdateUser({ + appUserId: userId, + appId, + }); - expect(errors).toBeUndefined(); - expect(data).toMatchSnapshot(); + expect(isCreated).toBe(false); + expect(user).toMatchSnapshot(); const { - body: { _source: user }, + body: { _source: source }, } = await client.get({ index: 'users', type: 'doc', id, }); - expect(user).toMatchSnapshot(); + expect(source).toMatchSnapshot(); expect(rollbar.error.mock.calls).toMatchSnapshot(); }); }); diff --git a/src/graphql/mutations/__tests__/__snapshots__/CreateOrUpdateUser.js.snap b/src/graphql/mutations/__tests__/__snapshots__/CreateOrUpdateUser.js.snap index b7741182..7412a55d 100644 --- a/src/graphql/mutations/__tests__/__snapshots__/CreateOrUpdateUser.js.snap +++ b/src/graphql/mutations/__tests__/__snapshots__/CreateOrUpdateUser.js.snap @@ -2,12 +2,19 @@ exports[`CreateOrUpdateUser creates backend user if not existed 1`] = ` Object { - "CreateOrUpdateUser": Object { - "createdAt": "2020-10-10T00:00:00.000Z", - "id": "6LOqD_3gpe4ZVaxRvemf7KNTfm6y3WNBu1hbs-5MRdSWiWVss", - "name": "Friendly Neighborhood Spider Man", - "updatedAt": "2020-10-10T00:00:00.000Z", - }, + "_cursor": undefined, + "_score": undefined, + "appId": "TEST_BACKEND", + "appUserId": "testUser2", + "avatarData": "{\\"accessory\\":\\"mask\\"}", + "avatarType": "OpenPeeps", + "createdAt": "2020-10-10T00:00:00.000Z", + "highlight": undefined, + "id": "6LOqD_3gpe4ZVaxRvemf7KNTfm6y3WNBu1hbs-5MRdSWiWVss", + "inner_hits": undefined, + "lastActiveAt": "2020-10-10T00:00:00.000Z", + "name": "Friendly Neighborhood Spider Man", + "updatedAt": "2020-10-10T00:00:00.000Z", } `; @@ -26,10 +33,15 @@ Object { exports[`CreateOrUpdateUser logs error if collision occurs 1`] = ` Object { - "CreateOrUpdateUser": Object { - "id": "6LOqD_QabTT7XXTrsz7ybEa5PLfc0GfXlV578HYPhODPfSWc8", - "name": "test user 1", - }, + "_cursor": undefined, + "_score": undefined, + "appId": "TEST_BACKEND", + "appUserId": "testUser1", + "highlight": undefined, + "id": "6LOqD_QabTT7XXTrsz7ybEa5PLfc0GfXlV578HYPhODPfSWc8", + "inner_hits": undefined, + "lastActiveAt": "2020-10-10T01:00:00.000Z", + "name": "test user 1", } `; @@ -45,19 +57,22 @@ Object { exports[`CreateOrUpdateUser logs error if collision occurs 3`] = ` Array [ Array [ - "createBackendUserError: collision found! testUser1 and testUser2 both hash to 6LOqD_QabTT7XXTrsz7ybEa5PLfc0GfXlV578HYPhODPfSWc8", + "createBackendUserError: collision found! testUser1 and testUser3 both hash to 6LOqD_QabTT7XXTrsz7ybEa5PLfc0GfXlV578HYPhODPfSWc8", ], ] `; exports[`CreateOrUpdateUser updates backend users' last active time if user already existed 1`] = ` Object { - "CreateOrUpdateUser": Object { - "createdAt": null, - "id": "6LOqD_QabTT7XXTrsz7ybEa5PLfc0GfXlV578HYPhODPfSWc8", - "name": "test user 1", - "updatedAt": null, - }, + "_cursor": undefined, + "_score": undefined, + "appId": "TEST_BACKEND", + "appUserId": "testUser1", + "highlight": undefined, + "id": "6LOqD_QabTT7XXTrsz7ybEa5PLfc0GfXlV578HYPhODPfSWc8", + "inner_hits": undefined, + "lastActiveAt": "2020-10-10T01:00:00.000Z", + "name": "test user 1", } `; diff --git a/src/graphql/schema.js b/src/graphql/schema.js index 53491436..ea31661d 100644 --- a/src/graphql/schema.js +++ b/src/graphql/schema.js @@ -20,7 +20,6 @@ import CreateOrUpdateArticleReplyFeedback from './mutations/CreateOrUpdateArticl import CreateOrUpdateReplyRequestFeedback from './mutations/CreateOrUpdateReplyRequestFeedback'; import CreateOrUpdateArticleCategoryFeedback from './mutations/CreateOrUpdateArticleCategoryFeedback'; import CreateOrUpdateReplyRequest from './mutations/CreateOrUpdateReplyRequest'; -import CreateOrUpdateUser from './mutations/CreateOrUpdateUser'; import UpdateArticleReplyStatus from './mutations/UpdateArticleReplyStatus'; import UpdateArticleCategoryStatus from './mutations/UpdateArticleCategoryStatus'; import UpdateUser from './mutations/UpdateUser'; @@ -55,7 +54,6 @@ export default new GraphQLSchema({ CreateOrUpdateArticleReplyFeedback, CreateOrUpdateArticleCategoryFeedback, CreateOrUpdateReplyRequestFeedback, - CreateOrUpdateUser, UpdateArticleReplyStatus, UpdateArticleCategoryStatus, UpdateUser, diff --git a/src/index.js b/src/index.js index 6ccd7c56..962fd9bd 100644 --- a/src/index.js +++ b/src/index.js @@ -15,10 +15,9 @@ import schema from './graphql/schema'; import DataLoaders from './graphql/dataLoaders'; import { AUTH_ERROR_MSG } from './graphql/util'; import CookieStore from './CookieStore'; -import { isBackendApp } from 'graphql/models/User'; import { loginRouter, authRouter } from './auth'; import rollbar from './rollbarInstance'; -import { createOrUpdateBackendUser } from './graphql/mutations/CreateOrUpdateUser'; +import { createOrUpdateUser } from './graphql/mutations/CreateOrUpdateUser'; const app = new Koa(); const router = Router(); @@ -84,22 +83,22 @@ export const apolloServer = new ApolloServer({ introspection: true, // Allow introspection in production as well playground: true, context: async ({ ctx }) => { - let user = ctx.state.user || {}; - if (ctx.appId && isBackendApp(ctx.appId)) { - const { user: backendUser } = await createOrUpdateBackendUser({ + let currentUser = ctx.state.user || {}; + if (ctx.appId) { + const { user } = await createOrUpdateUser({ appUserId: ctx.query.userId, appId: ctx.appId, }); - user = { ...user, ...backendUser }; + currentUser = { ...currentUser, ...user }; } return { loaders: new DataLoaders(), // new loaders per request - user: user, + user: currentUser, // userId-appId pair // - userId: user.id, + userId: currentUser.id, appId: ctx.appId, }; }, From 675c218322a98550d2b30b111e1e262fe65b7563 Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Sun, 18 Oct 2020 08:05:18 +0800 Subject: [PATCH 07/20] update tests --- src/__fixtures__/index.js | 4 ++++ src/__tests__/index.js | 26 ++++++++++++++++++++++++++ src/graphql/models/User.js | 4 ++++ 3 files changed, 34 insertions(+) diff --git a/src/__fixtures__/index.js b/src/__fixtures__/index.js index f5848f88..a6e2a4a5 100644 --- a/src/__fixtures__/index.js +++ b/src/__fixtures__/index.js @@ -4,4 +4,8 @@ export default { appUserId: 'testUser2', appId: 'TEST_BACKEND', }, + '/users/doc/testUser1': { + name: 'test user 1', + appId: 'WEBSITE', + }, }; diff --git a/src/__tests__/index.js b/src/__tests__/index.js index 1a01726e..055e8d87 100644 --- a/src/__tests__/index.js +++ b/src/__tests__/index.js @@ -3,6 +3,7 @@ import client from 'util/client'; import { apolloServer } from '../index'; import fixtures from '../__fixtures__/index'; import { createTestClient } from 'apollo-server-testing'; +import MockDate from 'mockdate'; jest.mock('koa', () => { const Koa = jest.requireActual('koa'); @@ -17,12 +18,19 @@ describe('apolloServer', () => { const actualGraphQLServerOptions = apolloServer.graphQLServerOptions; const mockGraphQLServerOptions = ctx => async () => actualGraphQLServerOptions.call(apolloServer, { ctx }); + let now; beforeAll(async () => { + MockDate.set(1602288000000); + now = new Date().toISOString(); + apolloServer.app; await loadFixtures(fixtures); }); + afterAll(async () => { + MockDate.reset(); + await unloadFixtures(fixtures); await client.delete({ index: 'users', @@ -50,13 +58,19 @@ describe('apolloServer', () => { query: `{ GetUser { id + appId + appUserId name + lastActiveAt } }`, }); expect(errors).toBeUndefined(); expect(res).toMatchObject({ id: 'testUser1', + name: 'test user 1', + appId: 'WEBSITE', + lastActiveAt: now, }); }); @@ -78,7 +92,10 @@ describe('apolloServer', () => { query: `{ GetUser { id + appId + appUserId name + lastActiveAt } }`, }); @@ -86,6 +103,9 @@ describe('apolloServer', () => { expect(res).toMatchObject({ id: '6LOqD_3gpe4ZVaxRvemf7KNTfm6y3WNBu1hbs-5MRdSWiWVss', name: 'test user 2', + appId: 'TEST_BACKEND', + appUserId: 'testUser2', + lastActiveAt: now, }); }); @@ -107,7 +127,10 @@ describe('apolloServer', () => { query: `{ GetUser { id + appId + appUserId name + lastActiveAt } }`, }); @@ -115,6 +138,9 @@ describe('apolloServer', () => { expect(res).toMatchObject({ id: '6LOqD_gsUWLlGviSA4KFdKpsNncQfTYeueOl-DGx9fL6zCNeA', name: expect.any(String), + appId: 'TEST_BACKEND', + appUserId: 'testUser3', + lastActiveAt: now, }); }); }); diff --git a/src/graphql/models/User.js b/src/graphql/models/User.js index ee2d5f15..e8332ace 100644 --- a/src/graphql/models/User.js +++ b/src/graphql/models/User.js @@ -156,6 +156,10 @@ const User = new GraphQLObjectType({ avatarUrl: avatarResolver(), avatarData: { type: GraphQLString }, + // TODO: also enable these two fields for requests from the same app? + appId: currentUserOnlyField(GraphQLString), + appUserId: currentUserOnlyField(GraphQLString), + facebookId: currentUserOnlyField(GraphQLString), githubId: currentUserOnlyField(GraphQLString), twitterId: currentUserOnlyField(GraphQLString), From 0ffa924f3ea9c3394ae4f6b7291e4d1194244fa2 Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Sun, 18 Oct 2020 08:21:46 +0800 Subject: [PATCH 08/20] remove unused code --- src/graphql/mutations/CreateOrUpdateUser.js | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/graphql/mutations/CreateOrUpdateUser.js b/src/graphql/mutations/CreateOrUpdateUser.js index 5857a811..15128b36 100644 --- a/src/graphql/mutations/CreateOrUpdateUser.js +++ b/src/graphql/mutations/CreateOrUpdateUser.js @@ -1,5 +1,5 @@ import { assertUser } from 'graphql/util'; -import User, { +import { generatePseudonym, generateOpenPeepsAvatar, AvatarTypes, @@ -61,17 +61,3 @@ export async function createOrUpdateUser({ appUserId, appId }) { isCreated, }; } - -export default { - description: 'Create or update a user for the given appId, appUserId pair', - type: User, - args: {}, - - async resolve(rootValue, _, { appId, userId }) { - const { user } = await createOrUpdateUser({ - appId, - appUserId: userId, - }); - return user; - }, -}; From df923ad6af3759fae3257c1486ad2cc9106026af Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Fri, 30 Oct 2020 16:40:33 +0800 Subject: [PATCH 09/20] ensure auth check is working --- .env.sample | 4 +- .gitignore | 7 +- src/__tests__/auth.js | 13 +- src/__tests__/index.js | 81 +++--- src/auth.js | 12 +- src/graphql/models/User.js | 8 +- src/graphql/models/__tests__/User.js | 241 +++++++++++------- .../__tests__/__snapshots__/User.js.snap | 38 +-- src/graphql/mutations/CreateOrUpdateUser.js | 9 +- .../mutations/__tests__/CreateOrUpdateUser.js | 5 + src/index.js | 27 +- .../__tests__/createBackendUsers.js | 3 - test/setup.js | 1 + 13 files changed, 237 insertions(+), 212 deletions(-) diff --git a/.env.sample b/.env.sample index 84e54344..827dba26 100644 --- a/.env.sample +++ b/.env.sample @@ -49,7 +49,7 @@ GITHUB_CLIENT_ID=YOUR_GITHUB_CLIENT_ID GITHUB_SECRET=YOUR_GITHUB_CLIENT_SECRET GITHUB_CALLBACK_URL=http://localhost:5000/callback/github -#Google Analytics Related settings +# Google Analytics Related settings GOOGLE_OAUTH_KEY_PATH=PATH_TO_SERVICE_ACCOUNT_KEY GA_PAGE_SIZE=10000 GA_WEB_VIEW_ID=GA_WEB_VIEW_ID @@ -64,3 +64,5 @@ URL_RESOLVER_URL=http://localhost:4000 ENGINE_API_KEY= WEB_CONCURRENCY=2 + +JEST_TIMEOUT=5000 \ No newline at end of file diff --git a/.gitignore b/.gitignore index fe2e55c9..efafc114 100644 --- a/.gitignore +++ b/.gitignore @@ -38,9 +38,10 @@ jspm_packages # Optional REPL history .node_repl_history -config/local-* - # docker-compose test data esdata -.env \ No newline at end of file +# local config +config/local-* +.vscode/ +.env diff --git a/src/__tests__/auth.js b/src/__tests__/auth.js index 9e008dcf..61cabf6b 100644 --- a/src/__tests__/auth.js +++ b/src/__tests__/auth.js @@ -7,11 +7,8 @@ import client from 'util/client'; const FIXED_DATE = 612921600000; describe('verifyProfile', () => { - beforeAll(() => loadFixtures(fixtures)); - afterAll(async () => { - await unloadFixtures(fixtures); - await client.delete({ index: 'users', type: 'doc', id: 'another-fb-id' }); - }); + beforeAll(async () => loadFixtures(fixtures)); + afterAll(async () => unloadFixtures(fixtures)); it('authenticates user via profile ID', async () => { const passportProfile = { @@ -52,7 +49,11 @@ describe('verifyProfile', () => { 'facebookId' ); MockDate.reset(); - + await client.delete({ + index: 'users', + type: 'doc', + id: id, + }); expect(newUser).toMatchSnapshot(); }); }); diff --git a/src/__tests__/index.js b/src/__tests__/index.js index 055e8d87..f60fa272 100644 --- a/src/__tests__/index.js +++ b/src/__tests__/index.js @@ -20,11 +20,31 @@ describe('apolloServer', () => { actualGraphQLServerOptions.call(apolloServer, { ctx }); let now; + const getCurrentUser = async (ctx = {}) => { + apolloServer.graphQLServerOptions = mockGraphQLServerOptions(ctx); + + const testClient = createTestClient(apolloServer); + const { + data: { GetUser: res }, + errors, + } = await testClient.query({ + query: `{ + GetUser { + id + appId + appUserId + name + lastActiveAt + } + }`, + }); + return { res, errors }; + }; + beforeAll(async () => { MockDate.set(1602288000000); now = new Date().toISOString(); - apolloServer.app; await loadFixtures(fixtures); }); @@ -39,32 +59,23 @@ describe('apolloServer', () => { }); }); + it('gracefully handles no auth request', async () => { + const { res, errors } = await getCurrentUser(); + expect(errors).toBeUndefined(); + expect(res).toBe(null); + }); + it('resolves current web user', async () => { const appId = 'WEBSITE'; const userId = 'testUser1'; - apolloServer.graphQLServerOptions = mockGraphQLServerOptions({ + const { res, errors } = await getCurrentUser({ appId, userId, state: { user: { id: userId } }, query: { userId }, }); - const testClient = createTestClient(apolloServer); - const { - data: { GetUser: res }, - errors, - } = await testClient.query({ - query: `{ - GetUser { - id - appId - appUserId - name - lastActiveAt - } - }`, - }); expect(errors).toBeUndefined(); expect(res).toMatchObject({ id: 'testUser1', @@ -78,27 +89,13 @@ describe('apolloServer', () => { const appId = 'TEST_BACKEND'; const userId = 'testUser2'; - apolloServer.graphQLServerOptions = mockGraphQLServerOptions({ + const { res, errors } = await getCurrentUser({ appId, userId, state: {}, query: { userId }, }); - const testClient = createTestClient(apolloServer); - const { - data: { GetUser: res }, - errors, - } = await testClient.query({ - query: `{ - GetUser { - id - appId - appUserId - name - lastActiveAt - } - }`, - }); + expect(errors).toBeUndefined(); expect(res).toMatchObject({ id: '6LOqD_3gpe4ZVaxRvemf7KNTfm6y3WNBu1hbs-5MRdSWiWVss', @@ -113,27 +110,13 @@ describe('apolloServer', () => { const appId = 'TEST_BACKEND'; const userId = 'testUser3'; - apolloServer.graphQLServerOptions = mockGraphQLServerOptions({ + const { res, errors } = await getCurrentUser({ appId, userId, state: {}, query: { userId }, }); - const testClient = createTestClient(apolloServer); - const { - data: { GetUser: res }, - errors, - } = await testClient.query({ - query: `{ - GetUser { - id - appId - appUserId - name - lastActiveAt - } - }`, - }); + expect(errors).toBeUndefined(); expect(res).toMatchObject({ id: '6LOqD_gsUWLlGviSA4KFdKpsNncQfTYeueOl-DGx9fL6zCNeA', diff --git a/src/auth.js b/src/auth.js index e1828ace..0cc2336b 100644 --- a/src/auth.js +++ b/src/auth.js @@ -15,14 +15,17 @@ passport.serializeUser((user, done) => { /** * De-serialize and populates ctx.state.user */ -passport.deserializeUser(async (userId, done) => { +passport.deserializeUser((userId, done) => { try { + /* const { body: user } = await client.get({ index: 'users', type: 'doc', id: userId, }); done(null, processMeta(user)); + */ + done(null, { userId }); } catch (err) { done(err); } @@ -31,7 +34,7 @@ passport.deserializeUser(async (userId, done) => { /** * Common verify callback for all login strategies. * It tries first authenticating user with profile.id agains fieldName in DB. - * If not applicatble, search for existing users with their email. + * If not applicable, search for existing users with their email. * If still not applicable, create a user with currently given profile. * * @param {object} profile - passport profile object @@ -222,7 +225,10 @@ export const authRouter = Router() await next(); let basePath = ''; - if (ctx.session.appId === 'RUMORS_SITE') { + if ( + ctx.session.appId === 'RUMORS_SITE' || + ctx.session.appId === 'DEVELOPMENT_FRONTEND' + ) { const allowedOrigins = process.env.RUMORS_SITE_CORS_ORIGIN.split(','); basePath = allowedOrigins.find(o => o === ctx.session.origin); } diff --git a/src/graphql/models/User.js b/src/graphql/models/User.js index e8332ace..0b1bd452 100644 --- a/src/graphql/models/User.js +++ b/src/graphql/models/User.js @@ -120,11 +120,11 @@ export const convertAppUserIdToUserId = ({ appId, appUserId }) => { * @param {GraphQLScalarType | GraphQLObjectType} type * @param {function?} resolver - Use default resolver if not given. */ -const currentUserOnlyField = (type, resolver) => ({ +export const currentUserOnlyField = (type, resolver) => ({ type, description: 'Returns only for current user. Returns `null` otherwise.', resolve(user, arg, context, info) { - if (user.id !== context.user.id) return null; + if (!context.user || user.id !== context.user.id) return null; return resolver ? resolver(user, arg, context, info) : user[info.fieldName]; }, @@ -245,11 +245,11 @@ export const userFieldResolver = async ( } /* TODO: some unit tests are depending on this code block, need to clean up those tests and then - remove the following lines. */ + remove the following lines, and the corresponding unit test. */ // If the user comes from the same client as the root document, return the user id. // - if (context.appId === appId) return { id: userId }; + if (context.appId && context.appId === appId) return { id: userId }; // If not, this client is not allowed to see user. // diff --git a/src/graphql/models/__tests__/User.js b/src/graphql/models/__tests__/User.js index 4bf71094..3ca2494b 100644 --- a/src/graphql/models/__tests__/User.js +++ b/src/graphql/models/__tests__/User.js @@ -4,6 +4,7 @@ import { generatePseudonym, generateOpenPeepsAvatar, userFieldResolver, + currentUserOnlyField, } from '../User'; import { loadFixtures, unloadFixtures } from 'util/fixtures'; import DataLoaders from '../../dataLoaders'; @@ -14,128 +15,174 @@ jest.mock('lodash', () => ({ })); describe('User model', () => { - it('userFieldResolver returns the right user', async () => { - await loadFixtures(fixtures); + beforeAll(async () => loadFixtures(fixtures)); + afterAll(async () => unloadFixtures(fixtures)); - const loaders = new DataLoaders(); + describe('currentUserOnlyField', () => { + const user = { + id: 'testuser1', + name: 'test user 1', + appUserId: 'userTest1', + appId: 'TEST_BACKEND', + }; + const mockResolver = jest.fn().mockReturnValue('John Doe'); + + it('handles null current user gracefully', async () => { + const args = [user, 'arg', {}, { fieldName: 'name' }]; + expect( + await currentUserOnlyField('type', mockResolver).resolve(...args) + ).toBe(null); + expect(mockResolver).not.toHaveBeenCalled(); + + expect(await currentUserOnlyField('type').resolve(...args)).toBe(null); + }); + + it('returns requested field for current user', async () => { + const args = [user, 'arg', { user }, { fieldName: 'name' }]; + expect( + await currentUserOnlyField('type', mockResolver).resolve(...args) + ).toBe('John Doe'); + expect(mockResolver).toHaveBeenLastCalledWith(...args); + mockResolver.mockClear(); + expect(await currentUserOnlyField('type').resolve(...args)).toBe( + user.name + ); + }); + + it('does not return requested field for another user', async () => { + const args = [ + user, + 'arg', + { user: { id: 'foo' } }, + { fieldName: 'name' }, + ]; + expect( + await currentUserOnlyField('type', mockResolver).resolve(...args) + ).toBe(null); + expect(mockResolver).not.toHaveBeenCalled(); + + expect(await currentUserOnlyField('type').resolve(...args)).toBe(null); + }); + }); +}); + +describe('userFieldResolver', () => { + const loaders = new DataLoaders(); + const resolveUser = ({ userId, appId }, ctx = {}) => + userFieldResolver({ userId, appId }, {}, { loaders, ...ctx }); + + it('handles missing arguments gracefully', async () => { + expect(await resolveUser({ appId: 'WEBSITE' })).toBe(null); + expect(await resolveUser({ appId: 'TEST_BACKEND' })).toBe(null); + expect(await resolveUser({ userId: 'abc' })).toBe(null); + }); + + it('returns the right backend user given appUserId', async () => { expect( - await userFieldResolver( - { - appId: 'TEST_BACKEND', - userId: 'userTest1', - }, - {}, - { loaders } - ) + await resolveUser({ + appId: 'TEST_BACKEND', + userId: 'userTest1', + }) ).toMatchSnapshot(); + }); + it('returns the right backend user given db userId', async () => { expect( - await userFieldResolver( - { - userId: '6LOqD_z5A8BwUr4gh1P2gw_2zFU3IIrSchTSl-vemod7CChMU', - appId: 'TEST_BACKEND', - }, - {}, - { loaders } - ) + await resolveUser({ + userId: '6LOqD_z5A8BwUr4gh1P2gw_2zFU3IIrSchTSl-vemod7CChMU', + appId: 'TEST_BACKEND', + }) ).toMatchSnapshot(); + }); + it('returns the right web user given userId', async () => { expect( - await userFieldResolver( - { - userId: 'userTest2', - appId: 'WEBSITE', - }, - {}, - { loaders } - ) + await resolveUser({ + userId: 'userTest2', + appId: 'WEBSITE', + }) ).toMatchSnapshot(); + }); + it('returns appUserId only to requests from the same client', async () => { expect( - await userFieldResolver( - { - userId: 'userTest3', - appId: 'WEBSITE', - }, - {}, - { loaders } - ) + await resolveUser({ + userId: 'userTest3', + appId: 'WEBSITE', + }) ).toBe(null); expect( - await userFieldResolver( + await resolveUser( { userId: 'userTest3', appId: 'TEST_BACKEND', }, - {}, - { loaders, appId: 'TEST_BACKEND' } + { appId: 'TEST_BACKEND' } ) ).toStrictEqual({ id: 'userTest3' }); + }); +}); - await unloadFixtures(fixtures); +describe('pseudo name and avatar generators', () => { + it('should generate pseudo names for user', () => { + [ + 0, // adjectives + 1, // names + 2, // towns + 3, // separators + 4, // decorators + 44, // adjectives + 890, // names + 349, // towns + 17, // separators + 42, // decorators + ].forEach(index => sample.mockImplementationOnce(ary => ary[index])); + + expect(generatePseudonym()).toBe(`忠懇的信義區艾達`); + expect(generatePseudonym()).toBe('㊣來自金城✖一本正經的✖金城武㊣'); }); - describe('pseudo name and avatar generators', () => { - it('should generate pseudo names for user', () => { - [ - 0, // adjectives - 1, // names - 2, // towns - 3, // separators - 4, // decorators - 44, // adjectives - 890, // names - 349, // towns - 17, // separators - 42, // decorators - ].forEach(index => sample.mockImplementationOnce(ary => ary[index])); - - expect(generatePseudonym()).toBe(`忠懇的信義區艾達`); - expect(generatePseudonym()).toBe('㊣來自金城✖一本正經的✖金城武㊣'); + it('should generate avatars for user', () => { + [ + 5, // face + 12, // hair + 7, // body + 3, // accessory + 2, // facialHair + 9, // face + 0, // hair + 2, // body + ].forEach(index => sample.mockImplementationOnce(ary => ary[index])); + [ + 0, // with accessory or not + 0, // with facialHair or not + 1, // to flip image or not + 0.23, // backgroundColorIndex + 1, // with accessory or not + 1, // with facialHair or not + 0, // to flip image or not + 0.17, // backgroundColorIndex + ].forEach(r => random.mockReturnValueOnce(r)); + + expect(generateOpenPeepsAvatar()).toMatchObject({ + accessory: 'None', + body: 'PointingUp', + face: 'Contempt', + hair: 'HatHip', + facialHair: 'None', + backgroundColorIndex: 0.23, + flip: true, }); - it('should generate avatars for user', () => { - [ - 5, // face - 12, // hair - 7, // body - 3, // accessory - 2, // facialHair - 9, // face - 0, // hair - 2, // body - ].forEach(index => sample.mockImplementationOnce(ary => ary[index])); - [ - 0, // with accessory or not - 0, // with facialHair or not - 1, // to flip image or not - 0.23, // backgroundColorIndex - 1, // with accessory or not - 1, // with facialHair or not - 0, // to flip image or not - 0.17, // backgroundColorIndex - ].forEach(r => random.mockReturnValueOnce(r)); - - expect(generateOpenPeepsAvatar()).toMatchObject({ - accessory: 'None', - body: 'PointingUp', - face: 'Contempt', - hair: 'HatHip', - facialHair: 'None', - backgroundColorIndex: 0.23, - flip: true, - }); - - expect(generateOpenPeepsAvatar()).toMatchObject({ - accessory: 'SunglassWayfarer', - body: 'ButtonShirt', - face: 'EyesClosed', - hair: 'Afro', - facialHair: 'FullMajestic', - backgroundColorIndex: 0.17, - flip: false, - }); + expect(generateOpenPeepsAvatar()).toMatchObject({ + accessory: 'SunglassWayfarer', + body: 'ButtonShirt', + face: 'EyesClosed', + hair: 'Afro', + facialHair: 'FullMajestic', + backgroundColorIndex: 0.17, + flip: false, }); }); }); diff --git a/src/graphql/models/__tests__/__snapshots__/User.js.snap b/src/graphql/models/__tests__/__snapshots__/User.js.snap index f2e75a98..edbca118 100644 --- a/src/graphql/models/__tests__/__snapshots__/User.js.snap +++ b/src/graphql/models/__tests__/__snapshots__/User.js.snap @@ -1,39 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`User model userFieldResolver returns the right user 1`] = ` -Object { - "_cursor": undefined, - "_score": undefined, - "appId": "TEST_BACKEND", - "appUserId": "userTest1", - "highlight": undefined, - "id": "6LOqD_z5A8BwUr4gh1P2gw_2zFU3IIrSchTSl-vemod7CChMU", - "inner_hits": undefined, - "name": "test user 1", -} -`; +exports[`userFieldResolver returns the right backend user given appUserId 1`] = `null`; -exports[`User model userFieldResolver returns the right user 2`] = ` -Object { - "_cursor": undefined, - "_score": undefined, - "appId": "TEST_BACKEND", - "appUserId": "userTest1", - "highlight": undefined, - "id": "6LOqD_z5A8BwUr4gh1P2gw_2zFU3IIrSchTSl-vemod7CChMU", - "inner_hits": undefined, - "name": "test user 1", -} -`; +exports[`userFieldResolver returns the right backend user given db userId 1`] = `null`; -exports[`User model userFieldResolver returns the right user 3`] = ` -Object { - "_cursor": undefined, - "_score": undefined, - "appId": "WEBSITE", - "highlight": undefined, - "id": "userTest2", - "inner_hits": undefined, - "name": "test user 2", -} -`; +exports[`userFieldResolver returns the right web user given userId 1`] = `null`; diff --git a/src/graphql/mutations/CreateOrUpdateUser.js b/src/graphql/mutations/CreateOrUpdateUser.js index 15128b36..31b11481 100644 --- a/src/graphql/mutations/CreateOrUpdateUser.js +++ b/src/graphql/mutations/CreateOrUpdateUser.js @@ -4,6 +4,7 @@ import { generateOpenPeepsAvatar, AvatarTypes, getUserId, + isBackendApp, } from 'graphql/models/User'; import client, { processMeta } from 'util/client'; @@ -49,13 +50,19 @@ export async function createOrUpdateUser({ appUserId, appId }) { const isCreated = result === 'created'; const user = processMeta({ ...userFound, _id: userId }); - if (!isCreated && (user.appId !== appId || user.appUserId !== appUserId)) { + // checking for collision + if ( + !isCreated && + isBackendApp(appId) && + (user.appId !== appId || user.appUserId !== appUserId) + ) { const errorMessage = `collision found! ${ user.appUserId } and ${appUserId} both hash to ${userId}`; console.log(errorMessage); rollbar.error(`createBackendUserError: ${errorMessage}`); } + return { user, isCreated, diff --git a/src/graphql/mutations/__tests__/CreateOrUpdateUser.js b/src/graphql/mutations/__tests__/CreateOrUpdateUser.js index 82750a60..3de9da5d 100644 --- a/src/graphql/mutations/__tests__/CreateOrUpdateUser.js +++ b/src/graphql/mutations/__tests__/CreateOrUpdateUser.js @@ -62,6 +62,8 @@ describe('CreateOrUpdateUser', () => { id, }); expect(source).toMatchSnapshot(); + expect(rollbar.error).not.toHaveBeenCalled(); + rollbar.error.mockClear(); MockDate.reset(); }); @@ -89,6 +91,8 @@ describe('CreateOrUpdateUser', () => { id, }); expect(source).toMatchSnapshot(); + expect(rollbar.error).not.toHaveBeenCalled(); + rollbar.error.mockClear(); }); it('logs error if collision occurs', async () => { @@ -116,5 +120,6 @@ describe('CreateOrUpdateUser', () => { }); expect(source).toMatchSnapshot(); expect(rollbar.error.mock.calls).toMatchSnapshot(); + rollbar.error.mockClear(); }); }); diff --git a/src/index.js b/src/index.js index 962fd9bd..f7392a8f 100644 --- a/src/index.js +++ b/src/index.js @@ -83,23 +83,30 @@ export const apolloServer = new ApolloServer({ introspection: true, // Allow introspection in production as well playground: true, context: async ({ ctx }) => { - let currentUser = ctx.state.user || {}; - if (ctx.appId) { - const { user } = await createOrUpdateUser({ - appUserId: ctx.query.userId, - appId: ctx.appId, - }); - currentUser = { ...currentUser, ...user }; + const { + appId, + query: { userId: queryUserId } = {}, + state: { user: { userId: sessionUserId } = {} } = {}, + } = ctx; + + const userId = queryUserId ?? sessionUserId; + + let currentUser = null; + if (appId && userId) { + ({ user: currentUser } = await createOrUpdateUser({ + appUserId: userId, + appId: appId, + })); } - + return { loaders: new DataLoaders(), // new loaders per request user: currentUser, // userId-appId pair // - userId: currentUser.id, - appId: ctx.appId, + userId, + appId, }; }, formatError(err) { diff --git a/src/scripts/migrations/__tests__/createBackendUsers.js b/src/scripts/migrations/__tests__/createBackendUsers.js index fdf93f67..9976ce8d 100644 --- a/src/scripts/migrations/__tests__/createBackendUsers.js +++ b/src/scripts/migrations/__tests__/createBackendUsers.js @@ -3,9 +3,6 @@ import client from 'util/client'; import CreateBackendUsers from '../createBackendUsers'; import fixtures from '../__fixtures__/createBackendUsers'; import { sortBy } from 'lodash'; -jest.setTimeout(50000); - -jest.setTimeout(45000); const checkAllDocsForIndex = async index => { let res = {}; diff --git a/test/setup.js b/test/setup.js index 50b8080a..556f4cd7 100644 --- a/test/setup.js +++ b/test/setup.js @@ -1,5 +1,6 @@ import 'dotenv/config'; jest.mock(__dirname + '../../src/util/grpc'); +jest.setTimeout(process.env.JEST_TIMEOUT || 5000); expect.extend({ toBeNaN(received) { From 177693c345bcbbed7e442f1c71fec3bf3a075e0e Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Fri, 30 Oct 2020 18:33:15 +0800 Subject: [PATCH 10/20] clean up --- src/auth.js | 8 -- src/graphql/models/__tests__/User.js | 105 +++++++++--------- .../__tests__/__snapshots__/User.js.snap | 38 ++++++- src/index.js | 2 +- 4 files changed, 88 insertions(+), 65 deletions(-) diff --git a/src/auth.js b/src/auth.js index 0cc2336b..848a6ad0 100644 --- a/src/auth.js +++ b/src/auth.js @@ -17,14 +17,6 @@ passport.serializeUser((user, done) => { */ passport.deserializeUser((userId, done) => { try { - /* - const { body: user } = await client.get({ - index: 'users', - type: 'doc', - id: userId, - }); - done(null, processMeta(user)); - */ done(null, { userId }); } catch (err) { done(err); diff --git a/src/graphql/models/__tests__/User.js b/src/graphql/models/__tests__/User.js index 3ca2494b..668eaaca 100644 --- a/src/graphql/models/__tests__/User.js +++ b/src/graphql/models/__tests__/User.js @@ -15,8 +15,8 @@ jest.mock('lodash', () => ({ })); describe('User model', () => { - beforeAll(async () => loadFixtures(fixtures)); - afterAll(async () => unloadFixtures(fixtures)); + beforeAll(async () => await loadFixtures(fixtures)); + afterAll(async () => await unloadFixtures(fixtures)); describe('currentUserOnlyField', () => { const user = { @@ -64,66 +64,65 @@ describe('User model', () => { expect(await currentUserOnlyField('type').resolve(...args)).toBe(null); }); }); -}); -describe('userFieldResolver', () => { - const loaders = new DataLoaders(); - const resolveUser = ({ userId, appId }, ctx = {}) => - userFieldResolver({ userId, appId }, {}, { loaders, ...ctx }); + describe('userFieldResolver', () => { + const loaders = new DataLoaders(); + const resolveUser = ({ userId, appId }, ctx = {}) => + userFieldResolver({ userId, appId }, {}, { loaders, ...ctx }); - it('handles missing arguments gracefully', async () => { - expect(await resolveUser({ appId: 'WEBSITE' })).toBe(null); - expect(await resolveUser({ appId: 'TEST_BACKEND' })).toBe(null); - expect(await resolveUser({ userId: 'abc' })).toBe(null); - }); + it('handles missing arguments gracefully', async () => { + expect(await resolveUser({ appId: 'WEBSITE' })).toBe(null); + expect(await resolveUser({ appId: 'TEST_BACKEND' })).toBe(null); + expect(await resolveUser({ userId: 'abc' })).toBe(null); + }); - it('returns the right backend user given appUserId', async () => { - expect( - await resolveUser({ - appId: 'TEST_BACKEND', - userId: 'userTest1', - }) - ).toMatchSnapshot(); - }); + it('returns the right backend user given appUserId', async () => { + expect( + await resolveUser({ + appId: 'TEST_BACKEND', + userId: 'userTest1', + }) + ).toMatchSnapshot(); + }); - it('returns the right backend user given db userId', async () => { - expect( - await resolveUser({ - userId: '6LOqD_z5A8BwUr4gh1P2gw_2zFU3IIrSchTSl-vemod7CChMU', - appId: 'TEST_BACKEND', - }) - ).toMatchSnapshot(); - }); + it('returns the right backend user given db userId', async () => { + expect( + await resolveUser({ + userId: '6LOqD_z5A8BwUr4gh1P2gw_2zFU3IIrSchTSl-vemod7CChMU', + appId: 'TEST_BACKEND', + }) + ).toMatchSnapshot(); + }); - it('returns the right web user given userId', async () => { - expect( - await resolveUser({ - userId: 'userTest2', - appId: 'WEBSITE', - }) - ).toMatchSnapshot(); - }); + it('returns the right web user given userId', async () => { + expect( + await resolveUser({ + userId: 'userTest2', + appId: 'WEBSITE', + }) + ).toMatchSnapshot(); + }); - it('returns appUserId only to requests from the same client', async () => { - expect( - await resolveUser({ - userId: 'userTest3', - appId: 'WEBSITE', - }) - ).toBe(null); - - expect( - await resolveUser( - { + it('returns appUserId only to requests from the same client', async () => { + expect( + await resolveUser({ userId: 'userTest3', - appId: 'TEST_BACKEND', - }, - { appId: 'TEST_BACKEND' } - ) - ).toStrictEqual({ id: 'userTest3' }); + appId: 'WEBSITE', + }) + ).toBe(null); + + expect( + await resolveUser( + { + userId: 'userTest3', + appId: 'TEST_BACKEND', + }, + { appId: 'TEST_BACKEND' } + ) + ).toStrictEqual({ id: 'userTest3' }); + }); }); }); - describe('pseudo name and avatar generators', () => { it('should generate pseudo names for user', () => { [ diff --git a/src/graphql/models/__tests__/__snapshots__/User.js.snap b/src/graphql/models/__tests__/__snapshots__/User.js.snap index edbca118..a382da2b 100644 --- a/src/graphql/models/__tests__/__snapshots__/User.js.snap +++ b/src/graphql/models/__tests__/__snapshots__/User.js.snap @@ -1,7 +1,39 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`userFieldResolver returns the right backend user given appUserId 1`] = `null`; +exports[`User model userFieldResolver returns the right backend user given appUserId 1`] = ` +Object { + "_cursor": undefined, + "_score": undefined, + "appId": "TEST_BACKEND", + "appUserId": "userTest1", + "highlight": undefined, + "id": "6LOqD_z5A8BwUr4gh1P2gw_2zFU3IIrSchTSl-vemod7CChMU", + "inner_hits": undefined, + "name": "test user 1", +} +`; -exports[`userFieldResolver returns the right backend user given db userId 1`] = `null`; +exports[`User model userFieldResolver returns the right backend user given db userId 1`] = ` +Object { + "_cursor": undefined, + "_score": undefined, + "appId": "TEST_BACKEND", + "appUserId": "userTest1", + "highlight": undefined, + "id": "6LOqD_z5A8BwUr4gh1P2gw_2zFU3IIrSchTSl-vemod7CChMU", + "inner_hits": undefined, + "name": "test user 1", +} +`; -exports[`userFieldResolver returns the right web user given userId 1`] = `null`; +exports[`User model userFieldResolver returns the right web user given userId 1`] = ` +Object { + "_cursor": undefined, + "_score": undefined, + "appId": "WEBSITE", + "highlight": undefined, + "id": "userTest2", + "inner_hits": undefined, + "name": "test user 2", +} +`; diff --git a/src/index.js b/src/index.js index f7392a8f..29af05c0 100644 --- a/src/index.js +++ b/src/index.js @@ -98,7 +98,7 @@ export const apolloServer = new ApolloServer({ appId: appId, })); } - + return { loaders: new DataLoaders(), // new loaders per request user: currentUser, From 3e94754438ac27f24d5eb8b4f93acb051a86984e Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Mon, 2 Nov 2020 19:30:48 +0800 Subject: [PATCH 11/20] add check post tests for uncleaned docs --- package.json | 3 ++- src/graphql/mutations/__tests__/CreateReply.js | 4 ++++ test/postTest.js | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 test/postTest.js diff --git a/package.json b/package.json index 6f921593..ea1ca104 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "seed": "cd test/rumors-db && npm run seed", "pretest": "npm run rumors-db:test", "test": "NODE_ENV=test ELASTICSEARCH_URL=http://localhost:62223 jest --runInBand", - "start": "pm2 startOrRestart ecosystem.config.js --env production --no-daemon", + "posttest": "NODE_ENV=test ELASTICSEARCH_URL=http://localhost:62223 babel-node test/postTest.js", + "start": "pm2 startOrRestart ecosystem.config.js --env production --no-daemon", "lint": "eslint src/.", "lint:fix": "eslint --fix src/.", "rumors-db:pull": "cd test/rumors-db && git pull", diff --git a/src/graphql/mutations/__tests__/CreateReply.js b/src/graphql/mutations/__tests__/CreateReply.js index ddd2b7dd..a299ae52 100644 --- a/src/graphql/mutations/__tests__/CreateReply.js +++ b/src/graphql/mutations/__tests__/CreateReply.js @@ -135,7 +135,11 @@ describe('CreateReply', () => { type: 'doc', id: replyId, }); + expect(reply._source).toMatchSnapshot(); + + // Cleanup + await client.delete({ index: 'replies', type: 'doc', id: replyId }); }); it('should throw error since a reference is required for type !== NOT_ARTICLE', async () => { diff --git a/test/postTest.js b/test/postTest.js new file mode 100644 index 00000000..9bb85393 --- /dev/null +++ b/test/postTest.js @@ -0,0 +1,18 @@ +import main from 'scripts/cleanupUrls'; +import client from 'util/client'; + +const checkDocs = async () => { + const {body: {hits: {total, hits}}} = await client.search({ + _source: 'false' + }) + + if (total > 0) { + console.log('\x1b[33m'); + console.log('WARNING: test db is not cleaned up properly.'); + console.log(JSON.stringify(hits.map(d => `/${d._index}/${d._type}/${d._id}`), null, 2)); + console.log('\x1b[0m'); + } +} + +checkDocs(); + From 6f97134bb4f8124341fdc74de9486b9781af8509 Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Mon, 2 Nov 2020 19:37:05 +0800 Subject: [PATCH 12/20] cleanup tests --- .../__tests__/createBackendUsers.js | 19 +++++++---- test/util/fixtures.js | 33 ------------------- 2 files changed, 12 insertions(+), 40 deletions(-) diff --git a/src/scripts/migrations/__tests__/createBackendUsers.js b/src/scripts/migrations/__tests__/createBackendUsers.js index 9976ce8d..83d53373 100644 --- a/src/scripts/migrations/__tests__/createBackendUsers.js +++ b/src/scripts/migrations/__tests__/createBackendUsers.js @@ -1,4 +1,4 @@ -import { loadFixtures, clearIndices, saveStateForIndices } from 'util/fixtures'; +import { loadFixtures } from 'util/fixtures'; import client from 'util/client'; import CreateBackendUsers from '../createBackendUsers'; import fixtures from '../__fixtures__/createBackendUsers'; @@ -42,9 +42,6 @@ const indices = [ let dbStates = {}; describe('createBackendUsers', () => { beforeAll(async () => { - // storing the current db states to restore to after the test is completed - dbStates = await saveStateForIndices(indices); - await clearIndices(indices); await loadFixtures(fixtures.fixturesToLoad); await new CreateBackendUsers({ @@ -60,9 +57,17 @@ describe('createBackendUsers', () => { }); afterAll(async () => { - await clearIndices(indices); - // restore db states to prevent affecting other tests - await loadFixtures(dbStates); + for (const index of indices) { + await client.deleteByQuery({ + index, + body: { + query: { + match_all: {}, + }, + }, + refresh: 'true' + }) + } }); for (const index of indices) { diff --git a/test/util/fixtures.js b/test/util/fixtures.js index 6f2a9832..9c695dd6 100644 --- a/test/util/fixtures.js +++ b/test/util/fixtures.js @@ -60,36 +60,3 @@ export async function resetFrom(fixtureMap, key) { refresh: 'true', }); } - -export async function saveStateForIndices(indices) { - let states = {} - for (const index of indices) { - const { body: { hits: { hits } } } = await client.search({ - index, - body: { - query: { - match_all: {}, - }, - }, - size: 10000 - }) - for (const doc of hits) { - states[`/${doc._index}/${doc._type}/${doc._id}`] = { ...doc._source }; - } - } - return states -} - -export async function clearIndices(indices) { - for (const index of indices) { - await client.deleteByQuery({ - index, - body: { - query: { - match_all: {}, - }, - }, - refresh: 'true' - }) - } -} \ No newline at end of file From 6abb340ad1470d2d1ff43ba0564084bc7551a596 Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Tue, 3 Nov 2020 00:18:50 +0800 Subject: [PATCH 13/20] add post test and test sequencer --- package.json | 4 +++- .../mutations/__tests__/CreateOrUpdateUser.js | 20 ++++--------------- src/graphql/queries/__tests__/ListReplies.js | 18 ++++------------- test/postTest.js | 10 ++++++++-- test/setup.js | 8 ++++++++ test/testSequencer.js | 13 ++++++++++++ 6 files changed, 40 insertions(+), 33 deletions(-) create mode 100644 test/testSequencer.js diff --git a/package.json b/package.json index ea1ca104..dc832757 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,9 @@ ], "setupFilesAfterEnv": [ "./test/setup.js" - ] + ], + "testSequencer": "./test/testSequencer.js" + }, "engines": { "node": ">=12" diff --git a/src/graphql/mutations/__tests__/CreateOrUpdateUser.js b/src/graphql/mutations/__tests__/CreateOrUpdateUser.js index 3de9da5d..31a7b101 100644 --- a/src/graphql/mutations/__tests__/CreateOrUpdateUser.js +++ b/src/graphql/mutations/__tests__/CreateOrUpdateUser.js @@ -1,4 +1,4 @@ -import { loadFixtures, saveStateForIndices, clearIndices } from 'util/fixtures'; +import { loadFixtures, unloadFixtures } from 'util/fixtures'; import client from 'util/client'; import MockDate from 'mockdate'; import fixtures from '../__fixtures__/CreateOrUpdateUser'; @@ -19,25 +19,12 @@ jest.mock('../../models/User', () => { }; }); -jest.mock('../../../rollbarInstance', () => ({ - __esModule: true, - default: { error: jest.fn() }, -})); - let dbStates = {}; describe('CreateOrUpdateUser', () => { - beforeAll(async () => { - dbStates = await saveStateForIndices(['users']); - await clearIndices(['users']); - await loadFixtures(fixtures); - }); + beforeAll( () => loadFixtures(fixtures)); - afterAll(async () => { - await clearIndices(['users']); - // restore db states to prevent affecting other tests - await loadFixtures(dbStates); - }); + afterAll( () => unloadFixtures(fixtures)); it('creates backend user if not existed', async () => { MockDate.set(1602288000000); @@ -66,6 +53,7 @@ describe('CreateOrUpdateUser', () => { rollbar.error.mockClear(); MockDate.reset(); + await client.delete({index: 'users', type: 'doc', id }) }); it("updates backend users' last active time if user already existed", async () => { diff --git a/src/graphql/queries/__tests__/ListReplies.js b/src/graphql/queries/__tests__/ListReplies.js index e390ea48..25ed0c13 100644 --- a/src/graphql/queries/__tests__/ListReplies.js +++ b/src/graphql/queries/__tests__/ListReplies.js @@ -1,17 +1,11 @@ -import { loadFixtures, clearIndices, saveStateForIndices } from 'util/fixtures'; +import { loadFixtures, unloadFixtures } from 'util/fixtures'; import gql from 'util/GraphQL'; import { getCursor } from 'graphql/util'; import fixtures from '../__fixtures__/ListReplies'; -const indices = ['replies', 'urls']; -let dbStates; + describe('ListReplies', () => { - beforeAll(async () => { - // storing the current db states to restore to after the test is completed - dbStates = await saveStateForIndices(indices); - await clearIndices(indices); - await loadFixtures(fixtures); - }); + beforeAll( () => loadFixtures(fixtures)); it('lists all replies', async () => { expect( @@ -390,9 +384,5 @@ describe('ListReplies', () => { ).toMatchSnapshot(); }); - afterAll(async () => { - await clearIndices(indices); - // restore db states to prevent affecting other tests - await loadFixtures(dbStates); - }); + afterAll(() => unloadFixtures(fixtures) ); }); diff --git a/test/postTest.js b/test/postTest.js index 9bb85393..492759e0 100644 --- a/test/postTest.js +++ b/test/postTest.js @@ -9,10 +9,16 @@ const checkDocs = async () => { if (total > 0) { console.log('\x1b[33m'); console.log('WARNING: test db is not cleaned up properly.'); - console.log(JSON.stringify(hits.map(d => `/${d._index}/${d._type}/${d._id}`), null, 2)); + const docs = hits.map(d => `/${d._index}/${d._type}/${d._id}`) + console.log(JSON.stringify(docs, null, 2)); console.log('\x1b[0m'); + + for (const d of hits) { + await client.delete({index: d._index, type: d._type, id: d._id}) + } + throw new Error() } } -checkDocs(); +checkDocs() diff --git a/test/setup.js b/test/setup.js index 556f4cd7..bd699c0b 100644 --- a/test/setup.js +++ b/test/setup.js @@ -1,7 +1,15 @@ import 'dotenv/config'; + jest.mock(__dirname + '../../src/util/grpc'); + +jest.mock(__dirname + '../../src/rollbarInstance', () => ({ + __esModule: true, + default: { error: jest.fn() }, +})); + jest.setTimeout(process.env.JEST_TIMEOUT || 5000); + expect.extend({ toBeNaN(received) { const pass = isNaN(received); diff --git a/test/testSequencer.js b/test/testSequencer.js new file mode 100644 index 00000000..ef75eea1 --- /dev/null +++ b/test/testSequencer.js @@ -0,0 +1,13 @@ +const Sequencer = require('@jest/test-sequencer').default; + +class TestSequencer extends Sequencer { + sort(tests) { + // Test structure information + // https://github.com/facebook/jest/blob/6b8b1404a1d9254e7d5d90a8934087a9c9899dab/packages/jest-runner/src/types.ts#L17-L21 + const copyTests = Array.from(tests); + return copyTests.sort((testA, testB) => (testA.path > testB.path ? 1 : -1)); + } +} + + +module.exports = TestSequencer; \ No newline at end of file From 42a40f4980cf846cd4aa190c520da068342bc877 Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Tue, 3 Nov 2020 00:41:53 +0800 Subject: [PATCH 14/20] lint --- src/__tests__/auth.js | 4 ++-- .../dataLoaders/__tests__/analyticsLoaderFactory.js | 4 ++-- src/graphql/models/__tests__/User.js | 4 ++-- .../mutations/__tests__/CreateOrUpdateUser.js | 8 +++----- src/graphql/queries/__tests__/ListReplies.js | 5 ++--- src/scripts/__tests__/fetchStatsFromGA.js | 12 ++++++------ .../migrations/__tests__/createBackendUsers.js | 5 ++--- 7 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/__tests__/auth.js b/src/__tests__/auth.js index 61cabf6b..c42d923e 100644 --- a/src/__tests__/auth.js +++ b/src/__tests__/auth.js @@ -7,8 +7,8 @@ import client from 'util/client'; const FIXED_DATE = 612921600000; describe('verifyProfile', () => { - beforeAll(async () => loadFixtures(fixtures)); - afterAll(async () => unloadFixtures(fixtures)); + beforeAll(() => loadFixtures(fixtures)); + afterAll(() => unloadFixtures(fixtures)); it('authenticates user via profile ID', async () => { const passportProfile = { diff --git a/src/graphql/dataLoaders/__tests__/analyticsLoaderFactory.js b/src/graphql/dataLoaders/__tests__/analyticsLoaderFactory.js index e0bfb364..b91ab599 100644 --- a/src/graphql/dataLoaders/__tests__/analyticsLoaderFactory.js +++ b/src/graphql/dataLoaders/__tests__/analyticsLoaderFactory.js @@ -7,7 +7,7 @@ const loader = new DataLoaders(); MockDate.set(1578589200000); // 2020-01-10T01:00:00.000+08:00 describe('analyticsLoaderFactory', () => { - beforeAll(async () => await loadFixtures(fixtures)); + beforeAll(() => loadFixtures(fixtures)); it('should load last 31 days of data for given id', async () => { const res = await loader.analyticsLoader.load({ @@ -64,5 +64,5 @@ describe('analyticsLoaderFactory', () => { expect(error).toBe('docType is required'); }); - afterAll(async () => await unloadFixtures(fixtures)); + afterAll(() => unloadFixtures(fixtures)); }); diff --git a/src/graphql/models/__tests__/User.js b/src/graphql/models/__tests__/User.js index 668eaaca..6835fbff 100644 --- a/src/graphql/models/__tests__/User.js +++ b/src/graphql/models/__tests__/User.js @@ -15,8 +15,8 @@ jest.mock('lodash', () => ({ })); describe('User model', () => { - beforeAll(async () => await loadFixtures(fixtures)); - afterAll(async () => await unloadFixtures(fixtures)); + beforeAll(() => loadFixtures(fixtures)); + afterAll(() => unloadFixtures(fixtures)); describe('currentUserOnlyField', () => { const user = { diff --git a/src/graphql/mutations/__tests__/CreateOrUpdateUser.js b/src/graphql/mutations/__tests__/CreateOrUpdateUser.js index 31a7b101..58368c3c 100644 --- a/src/graphql/mutations/__tests__/CreateOrUpdateUser.js +++ b/src/graphql/mutations/__tests__/CreateOrUpdateUser.js @@ -19,12 +19,10 @@ jest.mock('../../models/User', () => { }; }); -let dbStates = {}; - describe('CreateOrUpdateUser', () => { - beforeAll( () => loadFixtures(fixtures)); + beforeAll(() => loadFixtures(fixtures)); - afterAll( () => unloadFixtures(fixtures)); + afterAll(() => unloadFixtures(fixtures)); it('creates backend user if not existed', async () => { MockDate.set(1602288000000); @@ -53,7 +51,7 @@ describe('CreateOrUpdateUser', () => { rollbar.error.mockClear(); MockDate.reset(); - await client.delete({index: 'users', type: 'doc', id }) + await client.delete({ index: 'users', type: 'doc', id }); }); it("updates backend users' last active time if user already existed", async () => { diff --git a/src/graphql/queries/__tests__/ListReplies.js b/src/graphql/queries/__tests__/ListReplies.js index 25ed0c13..32ed953c 100644 --- a/src/graphql/queries/__tests__/ListReplies.js +++ b/src/graphql/queries/__tests__/ListReplies.js @@ -3,9 +3,8 @@ import gql from 'util/GraphQL'; import { getCursor } from 'graphql/util'; import fixtures from '../__fixtures__/ListReplies'; - describe('ListReplies', () => { - beforeAll( () => loadFixtures(fixtures)); + beforeAll(() => loadFixtures(fixtures)); it('lists all replies', async () => { expect( @@ -384,5 +383,5 @@ describe('ListReplies', () => { ).toMatchSnapshot(); }); - afterAll(() => unloadFixtures(fixtures) ); + afterAll(() => unloadFixtures(fixtures)); }); diff --git a/src/scripts/__tests__/fetchStatsFromGA.js b/src/scripts/__tests__/fetchStatsFromGA.js index 35cc8ccf..11170f8a 100644 --- a/src/scripts/__tests__/fetchStatsFromGA.js +++ b/src/scripts/__tests__/fetchStatsFromGA.js @@ -61,7 +61,7 @@ describe('fetchStatsFromGA', () => { yargs.argvMock.mockReset(); }); - it('without any arugments', async () => { + it('without any arguments', async () => { yargs.argvMock.mockReturnValue({ useContentGroup: true, loadScript: false, @@ -73,7 +73,7 @@ describe('fetchStatsFromGA', () => { expect(storeScriptInDBMock).not.toHaveBeenCalled(); }); - it('with date arugments', async () => { + it('with date arguments', async () => { yargs.argvMock.mockReturnValue({ startDate: '2020-01-01', endDate: '2020-02-01', @@ -93,7 +93,7 @@ describe('fetchStatsFromGA', () => { expect(storeScriptInDBMock).not.toHaveBeenCalled(); }); - it('with loadScript arugments', async () => { + it('with loadScript arguments', async () => { yargs.argvMock.mockReturnValue({ loadScript: true, useContentGroup: true, @@ -105,7 +105,7 @@ describe('fetchStatsFromGA', () => { expect(storeScriptInDBMock).toHaveBeenCalled(); }); - it('with loadScript and date arugments', async () => { + it('with loadScript and date arguments', async () => { yargs.argvMock.mockReturnValue({ loadScript: true, startDate: '2020-01-01', @@ -474,8 +474,8 @@ describe('fetchStatsFromGA', () => { upsertDocStatsSpy.mockClear(); }); - afterAll(async () => - await client.delete_script({ id: fetchStatsFromGA.upsertScriptID })); + afterAll(() => + client.delete_script({ id: fetchStatsFromGA.upsertScriptID })); it('should aggregate rows of data', async () => { await fetchStatsFromGA.processReport( diff --git a/src/scripts/migrations/__tests__/createBackendUsers.js b/src/scripts/migrations/__tests__/createBackendUsers.js index 83d53373..515a6a03 100644 --- a/src/scripts/migrations/__tests__/createBackendUsers.js +++ b/src/scripts/migrations/__tests__/createBackendUsers.js @@ -39,7 +39,6 @@ const indices = [ 'analytics', ]; -let dbStates = {}; describe('createBackendUsers', () => { beforeAll(async () => { await loadFixtures(fixtures.fixturesToLoad); @@ -65,8 +64,8 @@ describe('createBackendUsers', () => { match_all: {}, }, }, - refresh: 'true' - }) + refresh: 'true', + }); } }); From 74fe3fa8a6949754ec9e310c1b652477cc47e159 Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Tue, 3 Nov 2020 14:45:01 +0800 Subject: [PATCH 15/20] travis should fail when exits with uncleaned db --- src/scripts/migrations/__tests__/createBackendUsers.js | 4 ++-- test/postTest.js | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/scripts/migrations/__tests__/createBackendUsers.js b/src/scripts/migrations/__tests__/createBackendUsers.js index 515a6a03..d45f72ff 100644 --- a/src/scripts/migrations/__tests__/createBackendUsers.js +++ b/src/scripts/migrations/__tests__/createBackendUsers.js @@ -56,7 +56,7 @@ describe('createBackendUsers', () => { }); afterAll(async () => { - for (const index of indices) { + /*for (const index of indices) { await client.deleteByQuery({ index, body: { @@ -66,7 +66,7 @@ describe('createBackendUsers', () => { }, refresh: 'true', }); - } + }*/ }); for (const index of indices) { diff --git a/test/postTest.js b/test/postTest.js index 492759e0..b57e9002 100644 --- a/test/postTest.js +++ b/test/postTest.js @@ -16,9 +16,8 @@ const checkDocs = async () => { for (const d of hits) { await client.delete({index: d._index, type: d._type, id: d._id}) } - throw new Error() + process.exit(1) } } checkDocs() - From 302ee3354b4d3517f8c970f41a0889c683904df6 Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Tue, 3 Nov 2020 15:28:38 +0800 Subject: [PATCH 16/20] fix test --- .../migrations/__tests__/createBackendUsers.js | 4 ++-- test/postTest.js | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/scripts/migrations/__tests__/createBackendUsers.js b/src/scripts/migrations/__tests__/createBackendUsers.js index d45f72ff..515a6a03 100644 --- a/src/scripts/migrations/__tests__/createBackendUsers.js +++ b/src/scripts/migrations/__tests__/createBackendUsers.js @@ -56,7 +56,7 @@ describe('createBackendUsers', () => { }); afterAll(async () => { - /*for (const index of indices) { + for (const index of indices) { await client.deleteByQuery({ index, body: { @@ -66,7 +66,7 @@ describe('createBackendUsers', () => { }, refresh: 'true', }); - }*/ + } }); for (const index of indices) { diff --git a/test/postTest.js b/test/postTest.js index b57e9002..cb200edf 100644 --- a/test/postTest.js +++ b/test/postTest.js @@ -1,8 +1,17 @@ import main from 'scripts/cleanupUrls'; import client from 'util/client'; + const checkDocs = async () => { - const {body: {hits: {total, hits}}} = await client.search({ + + const {body: aliases} = await client.cat.aliases({ + format: 'JSON' + }) + + const indices = aliases.map(i => i.alias); + + const { body: { hits: { total, hits } } } = await client.search({ + index: indices, _source: 'false' }) From b49b3418f37a71b5f0359be349b52eb0312542c8 Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Sun, 8 Nov 2020 00:59:35 +0800 Subject: [PATCH 17/20] indent --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index dc832757..e0fa6a42 100644 --- a/package.json +++ b/package.json @@ -85,8 +85,7 @@ "setupFilesAfterEnv": [ "./test/setup.js" ], - "testSequencer": "./test/testSequencer.js" - + "testSequencer": "./test/testSequencer.js" }, "engines": { "node": ">=12" From ac363e5818840224f09216b55262aad1e8b74d56 Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Sun, 8 Nov 2020 00:52:10 +0800 Subject: [PATCH 18/20] rename appUserID to userID --- src/graphql/models/User.js | 20 +++++++++---------- src/graphql/mutations/CreateOrUpdateUser.js | 18 ++++++++--------- .../mutations/__tests__/CreateOrUpdateUser.js | 12 +++++------ src/index.js | 4 ++-- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/graphql/models/User.js b/src/graphql/models/User.js index 0b1bd452..35b64d62 100644 --- a/src/graphql/models/User.js +++ b/src/graphql/models/User.js @@ -70,22 +70,22 @@ export const generateOpenPeepsAvatar = () => { }; /** - * Given appId, appUserId pair, returns the id of corresponding user in db. + * Given appId, userId pair, where userId could be appUserId or dbUserID, returns the id of corresponding user in db. */ -export const getUserId = ({ appId, appUserId }) => { - if (!appId || !isBackendApp(appId) || isDBUserId({ appId, appUserId })) - return appUserId; - else return convertAppUserIdToUserId({ appId, appUserId }); +export const getUserId = ({ appId, userId }) => { + if (!appId || !isBackendApp(appId) || isDBUserId({ appId, userId })) + return userId; + else return convertAppUserIdToUserId({ appId, appUserId: userId }); }; /** * Check if the userId for a backend user is the user id in db or it is the app user Id. */ -export const isDBUserId = ({ appId, appUserId }) => +export const isDBUserId = ({ appId, userId }) => appId && - appUserId && - appUserId.length === BACKEND_USER_ID_LEN && - appUserId.substr(0, 6) === `${encodeAppId(appId)}_`; + userId && + userId.length === BACKEND_USER_ID_LEN && + userId.substr(0, 6) === `${encodeAppId(appId)}_`; export const encodeAppId = appId => crypto @@ -239,7 +239,7 @@ export const userFieldResolver = async ( // we can resolve user from userId. // if (userId && appId) { - const id = getUserId({ appId, appUserId: userId }); + const id = getUserId({ appId, userId }); const user = await loaders.docLoader.load({ index: 'users', id }); if (user) return user; } diff --git a/src/graphql/mutations/CreateOrUpdateUser.js b/src/graphql/mutations/CreateOrUpdateUser.js index 31b11481..645566bb 100644 --- a/src/graphql/mutations/CreateOrUpdateUser.js +++ b/src/graphql/mutations/CreateOrUpdateUser.js @@ -13,22 +13,22 @@ import rollbar from 'rollbarInstance'; /** * Index backend user if not existed, and record the last active time as now. * - * @param {string} appUserId - user ID given by an backend app + * @param {string} userID - either appUserID given by an backend app or userId for frontend users * @param {string} appId - app ID * * @returns {user: User, isCreated: boolean} */ -export async function createOrUpdateUser({ appUserId, appId }) { - assertUser({ appId, userId: appUserId }); +export async function createOrUpdateUser({ userId, appId }) { + assertUser({ appId, userId }); const now = new Date().toISOString(); - const userId = getUserId({ appId, appUserId }); + const dbUserId = getUserId({ appId, userId }); const { body: { result, get: userFound }, } = await client.update({ index: 'users', type: 'doc', - id: userId, + id: dbUserId, body: { doc: { lastActiveAt: now, @@ -38,7 +38,7 @@ export async function createOrUpdateUser({ appUserId, appId }) { avatarType: AvatarTypes.OpenPeeps, avatarData: JSON.stringify(generateOpenPeepsAvatar()), appId, - appUserId, + appUserId: userId, createdAt: now, updatedAt: now, lastActiveAt: now, @@ -48,17 +48,17 @@ export async function createOrUpdateUser({ appUserId, appId }) { }); const isCreated = result === 'created'; - const user = processMeta({ ...userFound, _id: userId }); + const user = processMeta({ ...userFound, _id: dbUserId }); // checking for collision if ( !isCreated && isBackendApp(appId) && - (user.appId !== appId || user.appUserId !== appUserId) + (user.appId !== appId || user.appUserId !== userId) ) { const errorMessage = `collision found! ${ user.appUserId - } and ${appUserId} both hash to ${userId}`; + } and ${userId} both hash to ${dbUserId}`; console.log(errorMessage); rollbar.error(`createBackendUserError: ${errorMessage}`); } diff --git a/src/graphql/mutations/__tests__/CreateOrUpdateUser.js b/src/graphql/mutations/__tests__/CreateOrUpdateUser.js index 58368c3c..f223842f 100644 --- a/src/graphql/mutations/__tests__/CreateOrUpdateUser.js +++ b/src/graphql/mutations/__tests__/CreateOrUpdateUser.js @@ -30,14 +30,14 @@ describe('CreateOrUpdateUser', () => { const appId = 'TEST_BACKEND'; const { user, isCreated } = await createOrUpdateUser({ - appUserId: userId, + userId, appId, }); expect(isCreated).toBe(true); expect(user).toMatchSnapshot(); - const id = getUserId({ appUserId: userId, appId }); + const id = getUserId({ userId, appId }); const { body: { _source: source }, @@ -61,14 +61,14 @@ describe('CreateOrUpdateUser', () => { const appId = 'TEST_BACKEND'; const { user, isCreated } = await createOrUpdateUser({ - appUserId: userId, + userId, appId, }); expect(isCreated).toBe(false); expect(user).toMatchSnapshot(); - const id = getUserId({ appUserId: userId, appId }); + const id = getUserId({ userId, appId }); const { body: { _source: source }, } = await client.get({ @@ -86,11 +86,11 @@ describe('CreateOrUpdateUser', () => { const userId = 'testUser3'; const appId = 'TEST_BACKEND'; - const id = getUserId({ appUserId: 'testUser1', appId }); + const id = getUserId({ userId: 'testUser1', appId }); getUserId.mockReturnValueOnce(id); const { user, isCreated } = await createOrUpdateUser({ - appUserId: userId, + userId, appId, }); diff --git a/src/index.js b/src/index.js index 29af05c0..b25b4e3d 100644 --- a/src/index.js +++ b/src/index.js @@ -94,8 +94,8 @@ export const apolloServer = new ApolloServer({ let currentUser = null; if (appId && userId) { ({ user: currentUser } = await createOrUpdateUser({ - appUserId: userId, - appId: appId, + userId, + appId, })); } From e252316a47bbd60b877bcb006533ef3cdf75ebe8 Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Sun, 8 Nov 2020 02:37:18 +0800 Subject: [PATCH 19/20] refactor user related helper functions to its own util file --- src/graphql/models/User.js | 108 +--------- src/graphql/models/__tests__/User.js | 75 +------ src/graphql/mutations/CreateOrUpdateUser.js | 70 ------- .../mutations/__tests__/CreateOrUpdateUser.js | 111 ---------- src/index.js | 2 +- src/scripts/migrations/createBackendUsers.js | 6 +- .../__fixtures__/user.js} | 0 .../__tests__/__snapshots__/user.js.snap} | 14 +- src/util/__tests__/user.js | 192 ++++++++++++++++++ src/util/user.js | 169 +++++++++++++++ 10 files changed, 373 insertions(+), 374 deletions(-) delete mode 100644 src/graphql/mutations/CreateOrUpdateUser.js delete mode 100644 src/graphql/mutations/__tests__/CreateOrUpdateUser.js rename src/{graphql/mutations/__fixtures__/CreateOrUpdateUser.js => util/__fixtures__/user.js} (100%) rename src/{graphql/mutations/__tests__/__snapshots__/CreateOrUpdateUser.js.snap => util/__tests__/__snapshots__/user.js.snap} (75%) create mode 100644 src/util/__tests__/user.js create mode 100644 src/util/user.js diff --git a/src/graphql/models/User.js b/src/graphql/models/User.js index 35b64d62..964f5e66 100644 --- a/src/graphql/models/User.js +++ b/src/graphql/models/User.js @@ -5,113 +5,7 @@ import { GraphQLNonNull, } from 'graphql'; import crypto from 'crypto'; -import { - adjectives, - names, - towns, - separators, - decorators, -} from 'util/pseudonymDict'; -import { - accessories, - faces, - facialHairStyles, - hairStyles, - bustPoses, -} from 'util/openPeepsOptions'; -import { sample, random } from 'lodash'; - -/** - * Generates a pseudonym. - */ -export const generatePseudonym = () => { - const [adj, name, place, separator, decorator] = [ - adjectives, - names, - towns, - separators, - decorators, - ].map(ary => sample(ary)); - return decorator(separator({ adj, name, place })); -}; - -export const AvatarTypes = { - OpenPeeps: 'OpenPeeps', -}; - -export const isBackendApp = appId => - appId !== 'WEBSITE' && appId !== 'DEVELOPMENT_FRONTEND'; - -// 6 for appId prefix and 43 for 256bit hashed userId with base64 encoding. -const BACKEND_USER_ID_LEN = 6 + 43; - -/** - * Generates data for open peeps avatar. - */ -export const generateOpenPeepsAvatar = () => { - const accessory = random() ? sample(accessories) : 'None'; - const facialHair = random() ? sample(facialHairStyles) : 'None'; - const flip = !!random(); - const backgroundColorIndex = random(0, 1, true); - - const face = sample(faces); - const hair = sample(hairStyles); - const body = sample(bustPoses); - - return { - accessory, - body, - face, - hair, - facialHair, - backgroundColorIndex, - flip, - }; -}; - -/** - * Given appId, userId pair, where userId could be appUserId or dbUserID, returns the id of corresponding user in db. - */ -export const getUserId = ({ appId, userId }) => { - if (!appId || !isBackendApp(appId) || isDBUserId({ appId, userId })) - return userId; - else return convertAppUserIdToUserId({ appId, appUserId: userId }); -}; - -/** - * Check if the userId for a backend user is the user id in db or it is the app user Id. - */ -export const isDBUserId = ({ appId, userId }) => - appId && - userId && - userId.length === BACKEND_USER_ID_LEN && - userId.substr(0, 6) === `${encodeAppId(appId)}_`; - -export const encodeAppId = appId => - crypto - .createHash('md5') - .update(appId) - .digest('base64') - .replace(/[+/]/g, '') - .substr(0, 5); - -export const sha256 = value => - crypto - .createHash('sha256') - .update(value) - .digest('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=+$/, ''); - -/** - * @param {string} appUserId - user ID given by an backend app - * @param {string} appId - app ID - * @returns {string} the id used to index `user` in db - */ -export const convertAppUserIdToUserId = ({ appId, appUserId }) => { - return `${encodeAppId(appId)}_${sha256(appUserId)}`; -}; +import { getUserId } from 'util/user'; /** * Field config helper for current user only field. diff --git a/src/graphql/models/__tests__/User.js b/src/graphql/models/__tests__/User.js index 6835fbff..ae9a2b5c 100644 --- a/src/graphql/models/__tests__/User.js +++ b/src/graphql/models/__tests__/User.js @@ -1,19 +1,8 @@ import fixtures from '../__fixtures__/User'; -import { random, sample } from 'lodash'; -import { - generatePseudonym, - generateOpenPeepsAvatar, - userFieldResolver, - currentUserOnlyField, -} from '../User'; +import { userFieldResolver, currentUserOnlyField } from '../User'; import { loadFixtures, unloadFixtures } from 'util/fixtures'; import DataLoaders from '../../dataLoaders'; -jest.mock('lodash', () => ({ - random: jest.fn(), - sample: jest.fn(), -})); - describe('User model', () => { beforeAll(() => loadFixtures(fixtures)); afterAll(() => unloadFixtures(fixtures)); @@ -123,65 +112,3 @@ describe('User model', () => { }); }); }); -describe('pseudo name and avatar generators', () => { - it('should generate pseudo names for user', () => { - [ - 0, // adjectives - 1, // names - 2, // towns - 3, // separators - 4, // decorators - 44, // adjectives - 890, // names - 349, // towns - 17, // separators - 42, // decorators - ].forEach(index => sample.mockImplementationOnce(ary => ary[index])); - - expect(generatePseudonym()).toBe(`忠懇的信義區艾達`); - expect(generatePseudonym()).toBe('㊣來自金城✖一本正經的✖金城武㊣'); - }); - - it('should generate avatars for user', () => { - [ - 5, // face - 12, // hair - 7, // body - 3, // accessory - 2, // facialHair - 9, // face - 0, // hair - 2, // body - ].forEach(index => sample.mockImplementationOnce(ary => ary[index])); - [ - 0, // with accessory or not - 0, // with facialHair or not - 1, // to flip image or not - 0.23, // backgroundColorIndex - 1, // with accessory or not - 1, // with facialHair or not - 0, // to flip image or not - 0.17, // backgroundColorIndex - ].forEach(r => random.mockReturnValueOnce(r)); - - expect(generateOpenPeepsAvatar()).toMatchObject({ - accessory: 'None', - body: 'PointingUp', - face: 'Contempt', - hair: 'HatHip', - facialHair: 'None', - backgroundColorIndex: 0.23, - flip: true, - }); - - expect(generateOpenPeepsAvatar()).toMatchObject({ - accessory: 'SunglassWayfarer', - body: 'ButtonShirt', - face: 'EyesClosed', - hair: 'Afro', - facialHair: 'FullMajestic', - backgroundColorIndex: 0.17, - flip: false, - }); - }); -}); diff --git a/src/graphql/mutations/CreateOrUpdateUser.js b/src/graphql/mutations/CreateOrUpdateUser.js deleted file mode 100644 index 645566bb..00000000 --- a/src/graphql/mutations/CreateOrUpdateUser.js +++ /dev/null @@ -1,70 +0,0 @@ -import { assertUser } from 'graphql/util'; -import { - generatePseudonym, - generateOpenPeepsAvatar, - AvatarTypes, - getUserId, - isBackendApp, -} from 'graphql/models/User'; -import client, { processMeta } from 'util/client'; - -import rollbar from 'rollbarInstance'; - -/** - * Index backend user if not existed, and record the last active time as now. - * - * @param {string} userID - either appUserID given by an backend app or userId for frontend users - * @param {string} appId - app ID - * - * @returns {user: User, isCreated: boolean} - */ -export async function createOrUpdateUser({ userId, appId }) { - assertUser({ appId, userId }); - const now = new Date().toISOString(); - const dbUserId = getUserId({ appId, userId }); - - const { - body: { result, get: userFound }, - } = await client.update({ - index: 'users', - type: 'doc', - id: dbUserId, - body: { - doc: { - lastActiveAt: now, - }, - upsert: { - name: generatePseudonym(), - avatarType: AvatarTypes.OpenPeeps, - avatarData: JSON.stringify(generateOpenPeepsAvatar()), - appId, - appUserId: userId, - createdAt: now, - updatedAt: now, - lastActiveAt: now, - }, - _source: true, - }, - }); - - const isCreated = result === 'created'; - const user = processMeta({ ...userFound, _id: dbUserId }); - - // checking for collision - if ( - !isCreated && - isBackendApp(appId) && - (user.appId !== appId || user.appUserId !== userId) - ) { - const errorMessage = `collision found! ${ - user.appUserId - } and ${userId} both hash to ${dbUserId}`; - console.log(errorMessage); - rollbar.error(`createBackendUserError: ${errorMessage}`); - } - - return { - user, - isCreated, - }; -} diff --git a/src/graphql/mutations/__tests__/CreateOrUpdateUser.js b/src/graphql/mutations/__tests__/CreateOrUpdateUser.js deleted file mode 100644 index f223842f..00000000 --- a/src/graphql/mutations/__tests__/CreateOrUpdateUser.js +++ /dev/null @@ -1,111 +0,0 @@ -import { loadFixtures, unloadFixtures } from 'util/fixtures'; -import client from 'util/client'; -import MockDate from 'mockdate'; -import fixtures from '../__fixtures__/CreateOrUpdateUser'; -import rollbar from 'rollbarInstance'; -import { getUserId } from 'graphql/models/User'; -import { createOrUpdateUser } from '../CreateOrUpdateUser'; - -jest.mock('../../models/User', () => { - const UserModel = jest.requireActual('../../models/User'); - return { - ...UserModel, - __esModule: true, - generatePseudonym: jest - .fn() - .mockReturnValue('Friendly Neighborhood Spider Man'), - generateOpenPeepsAvatar: jest.fn().mockReturnValue({ accessory: 'mask' }), - getUserId: jest.spyOn(UserModel, 'getUserId'), - }; -}); - -describe('CreateOrUpdateUser', () => { - beforeAll(() => loadFixtures(fixtures)); - - afterAll(() => unloadFixtures(fixtures)); - - it('creates backend user if not existed', async () => { - MockDate.set(1602288000000); - const userId = 'testUser2'; - const appId = 'TEST_BACKEND'; - - const { user, isCreated } = await createOrUpdateUser({ - userId, - appId, - }); - - expect(isCreated).toBe(true); - expect(user).toMatchSnapshot(); - - const id = getUserId({ userId, appId }); - - const { - body: { _source: source }, - } = await client.get({ - index: 'users', - type: 'doc', - id, - }); - expect(source).toMatchSnapshot(); - expect(rollbar.error).not.toHaveBeenCalled(); - rollbar.error.mockClear(); - - MockDate.reset(); - await client.delete({ index: 'users', type: 'doc', id }); - }); - - it("updates backend users' last active time if user already existed", async () => { - MockDate.set(1602291600000); - - const userId = 'testUser1'; - const appId = 'TEST_BACKEND'; - - const { user, isCreated } = await createOrUpdateUser({ - userId, - appId, - }); - - expect(isCreated).toBe(false); - expect(user).toMatchSnapshot(); - - const id = getUserId({ userId, appId }); - const { - body: { _source: source }, - } = await client.get({ - index: 'users', - type: 'doc', - id, - }); - expect(source).toMatchSnapshot(); - expect(rollbar.error).not.toHaveBeenCalled(); - rollbar.error.mockClear(); - }); - - it('logs error if collision occurs', async () => { - MockDate.set(1602291600000); - - const userId = 'testUser3'; - const appId = 'TEST_BACKEND'; - const id = getUserId({ userId: 'testUser1', appId }); - - getUserId.mockReturnValueOnce(id); - const { user, isCreated } = await createOrUpdateUser({ - userId, - appId, - }); - - expect(isCreated).toBe(false); - expect(user).toMatchSnapshot(); - - const { - body: { _source: source }, - } = await client.get({ - index: 'users', - type: 'doc', - id, - }); - expect(source).toMatchSnapshot(); - expect(rollbar.error.mock.calls).toMatchSnapshot(); - rollbar.error.mockClear(); - }); -}); diff --git a/src/index.js b/src/index.js index b25b4e3d..53348c89 100644 --- a/src/index.js +++ b/src/index.js @@ -17,7 +17,7 @@ import { AUTH_ERROR_MSG } from './graphql/util'; import CookieStore from './CookieStore'; import { loginRouter, authRouter } from './auth'; import rollbar from './rollbarInstance'; -import { createOrUpdateUser } from './graphql/mutations/CreateOrUpdateUser'; +import { createOrUpdateUser } from './util/user'; const app = new Koa(); const router = Router(); diff --git a/src/scripts/migrations/createBackendUsers.js b/src/scripts/migrations/createBackendUsers.js index f5d094df..c15209e3 100644 --- a/src/scripts/migrations/createBackendUsers.js +++ b/src/scripts/migrations/createBackendUsers.js @@ -41,7 +41,8 @@ import { generateOpenPeepsAvatar, AvatarTypes, convertAppUserIdToUserId, -} from 'graphql/models/User'; + isBackendApp, +} from 'util/user'; import { get, set } from 'lodash'; const AGG_NAME = 'userIdPair'; @@ -105,9 +106,6 @@ const backendUserQuery = { }, }; -const isBackendApp = appId => - appId !== 'WEBSITE' && appId !== 'DEVELOPMENT_FRONTEND'; - const userReferenceInSchema = { articlecategoryfeedbacks: { fields: [], diff --git a/src/graphql/mutations/__fixtures__/CreateOrUpdateUser.js b/src/util/__fixtures__/user.js similarity index 100% rename from src/graphql/mutations/__fixtures__/CreateOrUpdateUser.js rename to src/util/__fixtures__/user.js diff --git a/src/graphql/mutations/__tests__/__snapshots__/CreateOrUpdateUser.js.snap b/src/util/__tests__/__snapshots__/user.js.snap similarity index 75% rename from src/graphql/mutations/__tests__/__snapshots__/CreateOrUpdateUser.js.snap rename to src/util/__tests__/__snapshots__/user.js.snap index 7412a55d..ed0c0a98 100644 --- a/src/graphql/mutations/__tests__/__snapshots__/CreateOrUpdateUser.js.snap +++ b/src/util/__tests__/__snapshots__/user.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`CreateOrUpdateUser creates backend user if not existed 1`] = ` +exports[`user utils CreateOrUpdateUser creates backend user if not existed 1`] = ` Object { "_cursor": undefined, "_score": undefined, @@ -18,7 +18,7 @@ Object { } `; -exports[`CreateOrUpdateUser creates backend user if not existed 2`] = ` +exports[`user utils CreateOrUpdateUser creates backend user if not existed 2`] = ` Object { "appId": "TEST_BACKEND", "appUserId": "testUser2", @@ -31,7 +31,7 @@ Object { } `; -exports[`CreateOrUpdateUser logs error if collision occurs 1`] = ` +exports[`user utils CreateOrUpdateUser logs error if collision occurs 1`] = ` Object { "_cursor": undefined, "_score": undefined, @@ -45,7 +45,7 @@ Object { } `; -exports[`CreateOrUpdateUser logs error if collision occurs 2`] = ` +exports[`user utils CreateOrUpdateUser logs error if collision occurs 2`] = ` Object { "appId": "TEST_BACKEND", "appUserId": "testUser1", @@ -54,7 +54,7 @@ Object { } `; -exports[`CreateOrUpdateUser logs error if collision occurs 3`] = ` +exports[`user utils CreateOrUpdateUser logs error if collision occurs 3`] = ` Array [ Array [ "createBackendUserError: collision found! testUser1 and testUser3 both hash to 6LOqD_QabTT7XXTrsz7ybEa5PLfc0GfXlV578HYPhODPfSWc8", @@ -62,7 +62,7 @@ Array [ ] `; -exports[`CreateOrUpdateUser updates backend users' last active time if user already existed 1`] = ` +exports[`user utils CreateOrUpdateUser updates backend users' last active time if user already existed 1`] = ` Object { "_cursor": undefined, "_score": undefined, @@ -76,7 +76,7 @@ Object { } `; -exports[`CreateOrUpdateUser updates backend users' last active time if user already existed 2`] = ` +exports[`user utils CreateOrUpdateUser updates backend users' last active time if user already existed 2`] = ` Object { "appId": "TEST_BACKEND", "appUserId": "testUser1", diff --git a/src/util/__tests__/user.js b/src/util/__tests__/user.js new file mode 100644 index 00000000..60e7789f --- /dev/null +++ b/src/util/__tests__/user.js @@ -0,0 +1,192 @@ +import { loadFixtures, unloadFixtures } from 'util/fixtures'; +import client from 'util/client'; +import MockDate from 'mockdate'; +import fixtures from '../__fixtures__/user'; +import rollbar from 'rollbarInstance'; +import { random, sample } from 'lodash'; + +import { + generatePseudonym, + generateOpenPeepsAvatar, + getUserId, + createOrUpdateUser, +} from '../user'; + +jest.mock('lodash', () => { + const actualLodash = jest.requireActual('lodash'); + return { + random: jest.spyOn(actualLodash, 'random'), + sample: jest.spyOn(actualLodash, 'sample'), + }; +}); + +jest.mock('../user', () => { + const userUtils = jest.requireActual('../user'); + + return { + ...userUtils, + generatePseudonym: jest.spyOn(userUtils, 'generatePseudonym'), + generateOpenPeepsAvatar: jest.spyOn(userUtils, 'generateOpenPeepsAvatar'), + getUserId: jest.spyOn(userUtils, 'getUserId'), + }; +}); + +describe('user utils', () => { + describe('pseudo name and avatar generators', () => { + it('should generate pseudo names for user', () => { + [ + 0, // adjectives + 1, // names + 2, // towns + 3, // separators + 4, // decorators + 44, // adjectives + 890, // names + 349, // towns + 17, // separators + 42, // decorators + ].forEach(index => sample.mockImplementationOnce(ary => ary[index])); + + expect(generatePseudonym()).toBe(`忠懇的信義區艾達`); + expect(generatePseudonym()).toBe('㊣來自金城✖一本正經的✖金城武㊣'); + }); + + it('should generate avatars for user', () => { + [ + 5, // face + 12, // hair + 7, // body + 3, // accessory + 2, // facialHair + 9, // face + 0, // hair + 2, // body + ].forEach(index => sample.mockImplementationOnce(ary => ary[index])); + [ + 0, // with accessory or not + 0, // with facialHair or not + 1, // to flip image or not + 0.23, // backgroundColorIndex + 1, // with accessory or not + 1, // with facialHair or not + 0, // to flip image or not + 0.17, // backgroundColorIndex + ].forEach(r => random.mockReturnValueOnce(r)); + + expect(generateOpenPeepsAvatar()).toMatchObject({ + accessory: 'None', + body: 'PointingUp', + face: 'Contempt', + hair: 'HatHip', + facialHair: 'None', + backgroundColorIndex: 0.23, + flip: true, + }); + + expect(generateOpenPeepsAvatar()).toMatchObject({ + accessory: 'SunglassWayfarer', + body: 'ButtonShirt', + face: 'EyesClosed', + hair: 'Afro', + facialHair: 'FullMajestic', + backgroundColorIndex: 0.17, + flip: false, + }); + }); + }); + + describe('CreateOrUpdateUser', () => { + beforeAll(async () => { + await loadFixtures(fixtures); + generatePseudonym.mockReturnValue('Friendly Neighborhood Spider Man'); + generateOpenPeepsAvatar.mockReturnValue({ accessory: 'mask' }); + }); + + afterAll(() => unloadFixtures(fixtures)); + + it('creates backend user if not existed', async () => { + MockDate.set(1602288000000); + const userId = 'testUser2'; + const appId = 'TEST_BACKEND'; + + const { user, isCreated } = await createOrUpdateUser({ + userId, + appId, + }); + + expect(isCreated).toBe(true); + expect(user).toMatchSnapshot(); + + const id = getUserId({ userId, appId }); + + const { + body: { _source: source }, + } = await client.get({ + index: 'users', + type: 'doc', + id, + }); + expect(source).toMatchSnapshot(); + expect(rollbar.error).not.toHaveBeenCalled(); + rollbar.error.mockClear(); + + MockDate.reset(); + await client.delete({ index: 'users', type: 'doc', id }); + }); + + it("updates backend users' last active time if user already existed", async () => { + MockDate.set(1602291600000); + + const userId = 'testUser1'; + const appId = 'TEST_BACKEND'; + + const { user, isCreated } = await createOrUpdateUser({ + userId, + appId, + }); + + expect(isCreated).toBe(false); + expect(user).toMatchSnapshot(); + + const id = getUserId({ userId, appId }); + const { + body: { _source: source }, + } = await client.get({ + index: 'users', + type: 'doc', + id, + }); + expect(source).toMatchSnapshot(); + expect(rollbar.error).not.toHaveBeenCalled(); + rollbar.error.mockClear(); + }); + + it('logs error if collision occurs', async () => { + MockDate.set(1602291600000); + + const userId = 'testUser3'; + const appId = 'TEST_BACKEND'; + const id = getUserId({ userId: 'testUser1', appId }); + + getUserId.mockReturnValueOnce(id); + const { user, isCreated } = await createOrUpdateUser({ + userId, + appId, + }); + + expect(isCreated).toBe(false); + expect(user).toMatchSnapshot(); + + const { + body: { _source: source }, + } = await client.get({ + index: 'users', + type: 'doc', + id, + }); + expect(source).toMatchSnapshot(); + expect(rollbar.error.mock.calls).toMatchSnapshot(); + rollbar.error.mockClear(); + }); + }); +}); diff --git a/src/util/user.js b/src/util/user.js new file mode 100644 index 00000000..b12a3461 --- /dev/null +++ b/src/util/user.js @@ -0,0 +1,169 @@ +import { + adjectives, + names, + towns, + separators, + decorators, +} from 'util/pseudonymDict'; +import { + accessories, + faces, + facialHairStyles, + hairStyles, + bustPoses, +} from 'util/openPeepsOptions'; +import { sample, random } from 'lodash'; +import { assertUser } from 'graphql/util'; +import client, { processMeta } from 'util/client'; +import rollbar from 'rollbarInstance'; +import crypto from 'crypto'; + +/** + * Generates a pseudonym. + */ +export const generatePseudonym = () => { + const [adj, name, place, separator, decorator] = [ + adjectives, + names, + towns, + separators, + decorators, + ].map(ary => sample(ary)); + return decorator(separator({ adj, name, place })); +}; + +export const AvatarTypes = { + OpenPeeps: 'OpenPeeps', +}; + +export const isBackendApp = appId => + appId !== 'WEBSITE' && appId !== 'DEVELOPMENT_FRONTEND'; + +// 6 for appId prefix and 43 for 256bit hashed userId with base64 encoding. +const BACKEND_USER_ID_LEN = 6 + 43; + +/** + * Generates data for open peeps avatar. + */ +export const generateOpenPeepsAvatar = () => { + const accessory = random() ? sample(accessories) : 'None'; + const facialHair = random() ? sample(facialHairStyles) : 'None'; + const flip = !!random(); + const backgroundColorIndex = random(0, 1, true); + + const face = sample(faces); + const hair = sample(hairStyles); + const body = sample(bustPoses); + + return { + accessory, + body, + face, + hair, + facialHair, + backgroundColorIndex, + flip, + }; +}; + +/** + * Given appId, userId pair, where userId could be appUserId or dbUserID, returns the id of corresponding user in db. + */ +export const getUserId = ({ appId, userId }) => { + if (!appId || !isBackendApp(appId) || isDBUserId({ appId, userId })) + return userId; + else return convertAppUserIdToUserId({ appId, appUserId: userId }); +}; + +/** + * Check if the userId for a backend user is the user id in db or it is the app user Id. + */ +export const isDBUserId = ({ appId, userId }) => + appId && + userId && + userId.length === BACKEND_USER_ID_LEN && + userId.substr(0, 6) === `${encodeAppId(appId)}_`; + +export const encodeAppId = appId => + crypto + .createHash('md5') + .update(appId) + .digest('base64') + .replace(/[+/]/g, '') + .substr(0, 5); + +export const sha256 = value => + crypto + .createHash('sha256') + .update(value) + .digest('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + +/** + * @param {string} appUserId - user ID given by an backend app + * @param {string} appId - app ID + * @returns {string} the id used to index `user` in db + */ +export const convertAppUserIdToUserId = ({ appId, appUserId }) => { + return `${encodeAppId(appId)}_${sha256(appUserId)}`; +}; + +/** + * Index backend user if not existed, and record the last active time as now. + * + * @param {string} userID - either appUserID given by an backend app or userId for frontend users + * @param {string} appId - app ID + * + * @returns {user: User, isCreated: boolean} + */ +export async function createOrUpdateUser({ userId, appId }) { + assertUser({ appId, userId }); + const now = new Date().toISOString(); + const dbUserId = exports.getUserId({ appId, userId }); + const { + body: { result, get: userFound }, + } = await client.update({ + index: 'users', + type: 'doc', + id: dbUserId, + body: { + doc: { + lastActiveAt: now, + }, + upsert: { + name: exports.generatePseudonym(), + avatarType: AvatarTypes.OpenPeeps, + avatarData: JSON.stringify(exports.generateOpenPeepsAvatar()), + appId, + appUserId: userId, + createdAt: now, + updatedAt: now, + lastActiveAt: now, + }, + _source: true, + }, + }); + + const isCreated = result === 'created'; + const user = processMeta({ ...userFound, _id: dbUserId }); + + // checking for collision + if ( + !isCreated && + isBackendApp(appId) && + (user.appId !== appId || user.appUserId !== userId) + ) { + const errorMessage = `collision found! ${ + user.appUserId + } and ${userId} both hash to ${dbUserId}`; + console.log(errorMessage); + rollbar.error(`createBackendUserError: ${errorMessage}`); + } + + return { + user, + isCreated, + }; +} From c584d5b3a183e21a6fc5074433ee322d835c5b7a Mon Sep 17 00:00:00 2001 From: Zoe <1817638+ztsai@users.noreply.github.com> Date: Sun, 8 Nov 2020 03:05:13 +0800 Subject: [PATCH 20/20] move assertUser to user utils --- src/graphql/mutations/CreateArticle.js | 2 +- .../mutations/CreateArticleCategory.js | 2 +- src/graphql/mutations/CreateArticleReply.js | 2 +- src/graphql/mutations/CreateCategory.js | 2 +- .../CreateOrUpdateArticleCategoryFeedback.js | 2 +- .../CreateOrUpdateArticleReplyFeedback.js | 2 +- .../mutations/CreateOrUpdateReplyRequest.js | 2 +- .../CreateOrUpdateReplyRequestFeedback.js | 2 +- src/graphql/mutations/CreateReply.js | 2 +- src/graphql/util.js | 14 ------------- src/index.js | 3 +-- src/util/__tests__/user.js | 21 +++++++++++++++++++ src/util/user.js | 15 ++++++++++++- 13 files changed, 45 insertions(+), 26 deletions(-) diff --git a/src/graphql/mutations/CreateArticle.js b/src/graphql/mutations/CreateArticle.js index b29d14e7..ffff9e44 100644 --- a/src/graphql/mutations/CreateArticle.js +++ b/src/graphql/mutations/CreateArticle.js @@ -1,7 +1,7 @@ import { GraphQLString, GraphQLNonNull } from 'graphql'; import { h64 } from 'xxhashjs'; -import { assertUser } from 'graphql/util'; +import { assertUser } from 'util/user'; import client from 'util/client'; import scrapUrls from 'util/scrapUrls'; diff --git a/src/graphql/mutations/CreateArticleCategory.js b/src/graphql/mutations/CreateArticleCategory.js index 87c3aef1..2e77c942 100644 --- a/src/graphql/mutations/CreateArticleCategory.js +++ b/src/graphql/mutations/CreateArticleCategory.js @@ -6,7 +6,7 @@ import { } from 'graphql'; import client from 'util/client'; -import { assertUser } from 'graphql/util'; +import { assertUser } from 'util/user'; import ArticleCategory from 'graphql/models/ArticleCategory'; /** diff --git a/src/graphql/mutations/CreateArticleReply.js b/src/graphql/mutations/CreateArticleReply.js index f4ebfdc5..07dbd063 100644 --- a/src/graphql/mutations/CreateArticleReply.js +++ b/src/graphql/mutations/CreateArticleReply.js @@ -1,7 +1,7 @@ import { GraphQLString, GraphQLNonNull, GraphQLList } from 'graphql'; import client from 'util/client'; -import { assertUser } from 'graphql/util'; +import { assertUser } from 'util/user'; import ArticleReply from 'graphql/models/ArticleReply'; /** diff --git a/src/graphql/mutations/CreateCategory.js b/src/graphql/mutations/CreateCategory.js index a47aad40..bb0d77a7 100644 --- a/src/graphql/mutations/CreateCategory.js +++ b/src/graphql/mutations/CreateCategory.js @@ -1,6 +1,6 @@ import { GraphQLString, GraphQLNonNull } from 'graphql'; -import { assertUser } from 'graphql/util'; +import { assertUser } from 'util/user'; import client from 'util/client'; diff --git a/src/graphql/mutations/CreateOrUpdateArticleCategoryFeedback.js b/src/graphql/mutations/CreateOrUpdateArticleCategoryFeedback.js index 835f2310..e872e01a 100644 --- a/src/graphql/mutations/CreateOrUpdateArticleCategoryFeedback.js +++ b/src/graphql/mutations/CreateOrUpdateArticleCategoryFeedback.js @@ -1,6 +1,6 @@ import { GraphQLString, GraphQLNonNull } from 'graphql'; -import { assertUser } from 'graphql/util'; +import { assertUser } from 'util/user'; import FeedbackVote from 'graphql/models/FeedbackVote'; import ArticleCategory from 'graphql/models/ArticleCategory'; diff --git a/src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.js b/src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.js index e2084320..f409032c 100644 --- a/src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.js +++ b/src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.js @@ -1,6 +1,6 @@ import { GraphQLString, GraphQLNonNull } from 'graphql'; -import { assertUser } from 'graphql/util'; +import { assertUser } from 'util/user'; import FeedbackVote from 'graphql/models/FeedbackVote'; import ArticleReply from 'graphql/models/ArticleReply'; diff --git a/src/graphql/mutations/CreateOrUpdateReplyRequest.js b/src/graphql/mutations/CreateOrUpdateReplyRequest.js index 5a2b9afe..de9338b0 100644 --- a/src/graphql/mutations/CreateOrUpdateReplyRequest.js +++ b/src/graphql/mutations/CreateOrUpdateReplyRequest.js @@ -1,5 +1,5 @@ import { GraphQLString, GraphQLNonNull } from 'graphql'; -import { assertUser } from 'graphql/util'; +import { assertUser } from 'util/user'; import Article from '../models/Article'; import client, { processMeta } from 'util/client'; diff --git a/src/graphql/mutations/CreateOrUpdateReplyRequestFeedback.js b/src/graphql/mutations/CreateOrUpdateReplyRequestFeedback.js index b32162fb..1f0da682 100644 --- a/src/graphql/mutations/CreateOrUpdateReplyRequestFeedback.js +++ b/src/graphql/mutations/CreateOrUpdateReplyRequestFeedback.js @@ -1,6 +1,6 @@ import { GraphQLString, GraphQLNonNull } from 'graphql'; -import { assertUser } from 'graphql/util'; +import { assertUser } from 'util/user'; import FeedbackVote from 'graphql/models/FeedbackVote'; import ReplyRequest from 'graphql/models/ReplyRequest'; diff --git a/src/graphql/mutations/CreateReply.js b/src/graphql/mutations/CreateReply.js index 3072c022..7155b2d9 100644 --- a/src/graphql/mutations/CreateReply.js +++ b/src/graphql/mutations/CreateReply.js @@ -1,6 +1,6 @@ import { GraphQLString, GraphQLNonNull, GraphQLBoolean } from 'graphql'; -import { assertUser } from 'graphql/util'; +import { assertUser } from 'util/user'; import client from 'util/client'; import scrapUrls from 'util/scrapUrls'; diff --git a/src/graphql/util.js b/src/graphql/util.js index 06be9fa5..77805b36 100644 --- a/src/graphql/util.js +++ b/src/graphql/util.js @@ -418,20 +418,6 @@ export function createConnectionType( }); } -export const AUTH_ERROR_MSG = 'userId is not set via query string.'; - -export function assertUser({ userId, appId }) { - if (!userId) { - throw new Error(AUTH_ERROR_MSG); - } - - if (userId && !appId) { - throw new Error( - 'userId is set, but x-app-id or x-app-secret is not set accordingly.' - ); - } -} - export function filterArticleRepliesByStatus(articleReplies, status) { if (!status) return articleReplies; diff --git a/src/index.js b/src/index.js index 53348c89..6dc9a72d 100644 --- a/src/index.js +++ b/src/index.js @@ -13,11 +13,10 @@ import { formatError } from 'graphql'; import checkHeaders from './checkHeaders'; import schema from './graphql/schema'; import DataLoaders from './graphql/dataLoaders'; -import { AUTH_ERROR_MSG } from './graphql/util'; import CookieStore from './CookieStore'; import { loginRouter, authRouter } from './auth'; import rollbar from './rollbarInstance'; -import { createOrUpdateUser } from './util/user'; +import { AUTH_ERROR_MSG, createOrUpdateUser } from './util/user'; const app = new Koa(); const router = Router(); diff --git a/src/util/__tests__/user.js b/src/util/__tests__/user.js index 60e7789f..70b58e90 100644 --- a/src/util/__tests__/user.js +++ b/src/util/__tests__/user.js @@ -10,6 +10,8 @@ import { generateOpenPeepsAvatar, getUserId, createOrUpdateUser, + assertUser, + AUTH_ERROR_MSG, } from '../user'; jest.mock('lodash', () => { @@ -32,6 +34,25 @@ jest.mock('../user', () => { }); describe('user utils', () => { + describe('assertUser', () => { + it('should throw error if userId is not present', () => { + expect(() => assertUser({})).toThrow(AUTH_ERROR_MSG); + expect(() => assertUser({ appId: 'appId' })).toThrow(AUTH_ERROR_MSG); + }); + + it('should throw error if appId is not present', () => { + expect(() => assertUser({ userId: 'userId' })).toThrow( + 'userId is set, but x-app-id or x-app-secret is not set accordingly.' + ); + }); + + it('should not throw error if userId and appId are both present', () => { + expect(() => + assertUser({ appId: 'appId', userId: 'userId' }) + ).not.toThrow(); + }); + }); + describe('pseudo name and avatar generators', () => { it('should generate pseudo names for user', () => { [ diff --git a/src/util/user.js b/src/util/user.js index b12a3461..666825c4 100644 --- a/src/util/user.js +++ b/src/util/user.js @@ -13,11 +13,24 @@ import { bustPoses, } from 'util/openPeepsOptions'; import { sample, random } from 'lodash'; -import { assertUser } from 'graphql/util'; import client, { processMeta } from 'util/client'; import rollbar from 'rollbarInstance'; import crypto from 'crypto'; +export const AUTH_ERROR_MSG = 'userId is not set via query string.'; + +export function assertUser({ userId, appId }) { + if (!userId) { + throw new Error(AUTH_ERROR_MSG); + } + + if (userId && !appId) { + throw new Error( + 'userId is set, but x-app-id or x-app-secret is not set accordingly.' + ); + } +} + /** * Generates a pseudonym. */