diff --git a/helpers/encryption.js b/helpers/encryption.js index 0180e6e..a0f954e 100644 --- a/helpers/encryption.js +++ b/helpers/encryption.js @@ -4,7 +4,6 @@ const encryption = { encrypt: async (toEncrypt) => { await _sodium.ready; const sodium = _sodium; - // Get the key const key = sodium.from_base64(process.env.KEY); @@ -26,10 +25,6 @@ const encryption = { const key = sodium.from_base64(process.env.KEY); toDecryptWithNonce = sodium.from_base64(toDecryptWithNonce); - if (toDecryptWithNonce.length < sodium.crypto_secretbox_NONCEBYTES + sodium.crypto_secretbox_MACBYTES) { - throw 'Invalid cyphertext and/or nonce'; - } - const nonce = toDecryptWithNonce.slice(0, sodium.crypto_secretbox_NONCEBYTES); const ciphertext = toDecryptWithNonce.slice(sodium.crypto_secretbox_NONCEBYTES); const decrypted = sodium.crypto_secretbox_open_easy(ciphertext, nonce, key); diff --git a/resolvers/folders.js b/resolvers/folders.js index dfd47cf..7af31ab 100644 --- a/resolvers/folders.js +++ b/resolvers/folders.js @@ -23,17 +23,27 @@ const folderResolvers = { }, deleteFolder: ({ params }) => pool.query('DELETE FROM folder WHERE id = $1', [params.id]), - folderCounts: async ({ user }) => { + // If we're being tested, the testing function will pass in mocked + // versions of allFolders, allQueries & getUserUnseenCounts + // so we accept those, or fall back to the real ones if they are not passed + // See https://github.com/magicmark/jest-how-do-i-mock-x/blob/master/src/function-in-same-module/README.md + // for details on this DI approach + folderCounts: async ( + { user }, + _allFolders = folderResolvers.allFolders, + _allQueries = queries.allQueries, + _getUserUnseenCounts = queryuser.getUserUnseenCounts + ) => { try { // Fetch all the folders and all this user's queries - const [allFolders, allQueries] = await Promise.all([ - folderResolvers.allFolders(), - queries.allQueries({ query: {}, user }) + const [allFoldersRes, allQueriesRes] = await Promise.all([ + _allFolders(), + _allQueries({ query: {}, user }) ]); // We need an array of the user's queries - const queryIds = allQueries.rows.map((row) => row.id); + const queryIds = allQueriesRes.rows.map((row) => row.id); // Get the unseen counts for all of the user's queries - const unseenCounts = await queryuser.getUserUnseenCounts({ + const unseenCounts = await _getUserUnseenCounts({ query_ids: queryIds, user_id: user.id }); @@ -43,17 +53,17 @@ const folderResolvers = { ); const result = { UNREAD: queriesWithUnseen.length, - ALL_QUERIES: allQueries.rowCount - queriesWithUnseen.length + ALL_QUERIES: allQueriesRes.rowCount - queriesWithUnseen.length }; - allFolders.rows.forEach((folder) => { - const hasOccurences = allQueries.rows.filter( + allFoldersRes.rows.forEach((folder) => { + const hasOccurences = allQueriesRes.rows.filter( (query) => query.folder === folder.code ).length; result[folder.id] = hasOccurences; }); return Promise.resolve(result); } catch (err) { - Promise.reject(err); + return Promise.reject(err); } } }; diff --git a/resolvers/labels.js b/resolvers/labels.js index 084dd9e..4706284 100644 --- a/resolvers/labels.js +++ b/resolvers/labels.js @@ -25,30 +25,44 @@ const labelResolvers = { }, deleteLabel: ({ params }) => pool.query('DELETE FROM label WHERE id = $1', [params.id]), - labelCounts: async ({ user }) => { - // Fetch all the labels and all this user's queries - const [allLabels, allQueries] = await Promise.all([ - labelResolvers.allLabels(), - queries.allQueries({ query: {}, user }) - ]); - // Now retrieve all the label associations for this user's - // queries - const queryLabelsResult = await querylabel.allLabelsForQueries( - allQueries.rows.map((row) => row.id) - ); - const queryLabels = queryLabelsResult.rows; - // Iterate each label and determine how many queries are - // assigned to it. We're going to create an object keyed on label - // ID, with a value of the associated query count - const toSend = {}; - allLabels.rows.forEach((label) => { - const labelId = label.id; - const count = queryLabels.filter( - (queryLabel) => queryLabel.label_id === label.id - ).length; - toSend[labelId] = count; - }); - return toSend; + // If we're being tested, the testing function will pass in mocked + // versions of allLabels, allQueries & allLabelsForQueries + // so we accept those, or fall back to the real ones if they are not passed + // See https://github.com/magicmark/jest-how-do-i-mock-x/blob/master/src/function-in-same-module/README.md + // for details on this DI approach + labelCounts: async ( + { user }, + _allLabels = labelResolvers.allLabels, + _allQueries = queries.allQueries, + _allLabelsForQueries = querylabel.allLabelsForQueries + ) => { + try { + // Fetch all the labels and all this user's queries + const [allLabelsRes, allQueriesRes] = await Promise.all([ + _allLabels(), + _allQueries({ query: {}, user }) + ]); + // Now retrieve all the label associations for this user's + // queries + const queryLabelsResult = await _allLabelsForQueries( + allQueriesRes.rows.map((row) => row.id) + ); + const queryLabels = queryLabelsResult.rows; + // Iterate each label and determine how many queries are + // assigned to it. We're going to create an object keyed on label + // ID, with a value of the associated query count + const toSend = {}; + allLabelsRes.rows.forEach((label) => { + const labelId = label.id; + const count = queryLabels.filter( + (queryLabel) => queryLabel.label_id === label.id + ).length; + toSend[labelId] = count; + }); + return toSend; + } catch(err) { + return Promise.reject(err); + } } }; diff --git a/resolvers/queryuser.js b/resolvers/queryuser.js index baaa879..e0db002 100644 --- a/resolvers/queryuser.js +++ b/resolvers/queryuser.js @@ -19,11 +19,28 @@ const queryuserResolvers = { 'SELECT query_id, most_recent_seen FROM queryuser WHERE user_id = $1', [id] ), - calculateUnseenCount: async ({ query_id, user_id, mostRecentSeen }) => { - const unseen = await pool.query( + getUnseenCounts: async ({ query_id, user_id, mostRecentSeen }) => { + return await pool.query( 'SELECT count(*) AS rowcount FROM message WHERE query_id = $1 AND creator_id != $2 AND id > $3', [query_id, user_id, mostRecentSeen] ); + }, + // If we're being tested, the testing function will pass in a mocked + // version of getUnseenCounts so we accept that, or fall back to the + // real ones if they are not passed + // See https://github.com/magicmark/jest-how-do-i-mock-x/blob/master/src/function-in-same-module/README.md + // for details on this DI approach + calculateUnseenCount: async ({ + query_id, + user_id, + mostRecentSeen, + _getUnseenCounts = queryuserResolvers.getUnseenCounts + }) => { + const unseen = await _getUnseenCounts({ + query_id, + user_id, + mostRecentSeen + }); return pool.query( 'UPDATE queryuser SET unseen_count = $1 WHERE query_id = $2 AND user_id = $3 RETURNING *', [unseen.rows[0].rowcount, query_id, user_id] @@ -39,10 +56,19 @@ const queryuserResolvers = { }, // Upsert queryuser rows for a given query for all users except // the passed one - upsertQueryUsers: async ({ query_id, creator }) => { + // If we're being tested, the testing function will pass in a mocked + // version of associated so we accept that, or fall back to the + // real ones if they are not passed + // See https://github.com/magicmark/jest-how-do-i-mock-x/blob/master/src/function-in-same-module/README.md + // for details on this DI approach + upsertQueryUsers: async ({ + query_id, + creator, + _associated = queries.associated + }) => { // Determine who needs adding / updating. This needs to be anyone // who can see this query, i.e. participants & STAFF, except the sender - const users = await queries.associated([query_id]); + const users = await _associated([query_id]); // Remove the sender from the list of users needing // creating / updating const creatorIdx = users.findIndex(u => u === creator); @@ -85,14 +111,23 @@ const queryuserResolvers = { }, // Following the deletion of a message, decrement the unseen_count // of the appropriate users - decrementMessageDelete: async ({ message }) => { - const toDecrement = await queryuserResolvers.getUsersToDecrement({ + // If we're being tested, the testing function will pass in a mocked + // version of getUsersToDecrement & decrementUnseenCount + // so we accept those, or fall back to the real ones if they are not passed + // See https://github.com/magicmark/jest-how-do-i-mock-x/blob/master/src/function-in-same-module/README.md + // for details on this DI approach + decrementMessageDelete: async ({ + message, + _getUsersToDecrement = queryuserResolvers.getUsersToDecrement, + _decrementUnseenCount = queryuserResolvers.decrementUnseenCount + }) => { + const toDecrement = await _getUsersToDecrement({ query_id: message.query_id, exclude: [message.creator_id], most_recent_seen: message.id }); for (let i = 0; i < toDecrement.length; i++) { - queryuserResolvers.decrementUnseenCount({ + _decrementUnseenCount({ query_id: message.query_id, user_id: toDecrement[i] }); @@ -101,9 +136,19 @@ const queryuserResolvers = { // Given a query_id and (optional) exclude list and (optional) // most_recent_seen value, return the IDs of users to have their // unseen_count decremented - getUsersToDecrement: async ({ query_id, exclude, most_recent_seen }) => { + // If we're being tested, the testing function will pass in a mocked + // version of getParticipantCounts, so we accept that, + // or fall back to the real ones if they are not passed + // See https://github.com/magicmark/jest-how-do-i-mock-x/blob/master/src/function-in-same-module/README.md + // for details on this DI approach + getUsersToDecrement: async ({ + query_id, + exclude, + most_recent_seen, + _getParticipantCounts = queryuserResolvers.getParticipantCounts + }) => { // Get all users involved in this query - const toDecrement = await queryuserResolvers.getParticipantCounts( + const toDecrement = await _getParticipantCounts( { query_id } ); return toDecrement.rows @@ -128,12 +173,12 @@ const queryuserResolvers = { const placeholders = query_ids .map((param, idx) => `$${idx + 1}`) .join(', '); - optionalIn = `query_id IN (${placeholders}) AND`; + optionalIn = `query_id IN (${placeholders}) AND `; parameters = [...query_ids]; } parameters.push(user_id); return pool.query( - `SELECT query_id, unseen_count FROM queryuser WHERE ${optionalIn} user_id = $${parameters.length}`, + `SELECT query_id, unseen_count FROM queryuser WHERE ${optionalIn}user_id = $${parameters.length}`, parameters ); }, diff --git a/resolvers/users.js b/resolvers/users.js index 78eadb0..77f9446 100644 --- a/resolvers/users.js +++ b/resolvers/users.js @@ -43,7 +43,11 @@ const userResolvers = { 'SELECT * FROM ems_user WHERE provider = $1 AND provider_id = $2', [params.provider, params.providerId] ), - upsertUser: ({ params, body }) => { + upsertUser: ({ + params, + body, + _getRoleByCode = roleResolvers.getRoleByCode + }) => { // If we have an ID, we're updating if (params && params.id) { return pool.query( @@ -59,8 +63,7 @@ const userResolvers = { ); } else { // Give the new user the CUSTOMER role - return roleResolvers - .getRoleByCode({ params: { code: 'CUSTOMER' } }) + return _getRoleByCode({ params: { code: 'CUSTOMER' } }) .then((roles) => { if (roles.rowCount === 1) { const role = roles.rows[0].id; @@ -86,20 +89,22 @@ const userResolvers = { providerMeta, name, email, - avatar + avatar, + _encryption = encryption, + _getUserByProvider = userResolvers.getUserByProvider, + _upsertUser = userResolvers.upsertUser }) => { // If we've been provided with an email, we need to encrypt it if (email) { - email = await encryption.encrypt(email); + email = await _encryption.encrypt(email); } // Check if this user already exists - return userResolvers - .getUserByProvider({ params: { provider, providerId } }) + return _getUserByProvider({ params: { provider, providerId } }) .then((result) => { if (result.rowCount === 1) { // User exists, update them const user = result.rows[0]; - return userResolvers.upsertUser({ + return _upsertUser({ params: { id: user.id }, @@ -113,7 +118,7 @@ const userResolvers = { } }); } else { - return userResolvers.upsertUser({ + return _upsertUser({ body: { name, email, diff --git a/test/helpers/encryption.test.js b/test/helpers/encryption.test.js new file mode 100644 index 0000000..fd68693 --- /dev/null +++ b/test/helpers/encryption.test.js @@ -0,0 +1,30 @@ +// The thing we're testing +const { encrypt, decrypt } = require('../../helpers/encryption'); + +process.env.KEY = 'B4QEGpy_Ad_MjuEIAoSNWhegBHrNBItN2aV1Ua1g2A4'; + +describe('encypt', () => { + // For a given key, we receive something not null + // back. This is about as meaningful as we can hope to get with + // this test as what we get back is not predictable + it('should return a correctly encrpypted string', async (done) => { + const result = await encrypt('Death Star Plans'); + expect(result).not.toBeNull(); + done(); + }); +}); + +describe('decrypt', () => { + // For a given cipher text, check we get back what we expect + it('should return a correctly decrpypted string', async (done) => { + const result = await decrypt('9ec-OZZ6HZzE8gG9VheeNYlMT_rrvTD8Lg1LPjUjD1fyjq1F-4LcM4ufiqdlVCfzoW3k4sSp-O0'); + expect(result).toEqual('Death Star Plans'); + done(); + }); + it('should throw when passed an invalid cipher text', async (done) => { + await expect( + decrypt('9ac-OZZ6HZzE8gG9VheeNYlMT_rrvTD8Lg1LPjUjD1fyjq1F-4LcM4ufiqdlVCfzoW3k4sSp-O0') + ).rejects.toBeTruthy(); + done(); + }); +}); \ No newline at end of file diff --git a/test/resolvers/folders.test.js b/test/resolvers/folders.test.js index f7079e2..2edf1d4 100644 --- a/test/resolvers/folders.test.js +++ b/test/resolvers/folders.test.js @@ -77,4 +77,60 @@ describe('Folders', () => { done(); }); }); + describe('folderCounts', () => { + const mockedFn = jest.fn(() => + new Promise((resolve) => { + return resolve({ + rows: [ + { id: 1, unseen_count: 2, folder: 2, code: 2 }, + { id: 2, unseen_count: 0, folder: 1, code: 1 }, + { id: 3, unseen_count: 1, folder: 3, code: 3 } + ], + rowCount: 3 + }); + }) + ); + it( + 'allFolders, allQueries & getUserUnseenCounts should be called', + async (done) => { + await folders.folderCounts( + { user: { id: 1 } }, + mockedFn, + mockedFn, + mockedFn + ); + expect(mockedFn).toHaveBeenCalledTimes(3); + done(); + } + ); + it( + 'should return the correct result', + async (done) => { + const result = await folders.folderCounts( + { user: { id: 1 } }, + mockedFn, + mockedFn, + mockedFn + ); + expect(result).toEqual({ + '1': 1, + '2': 1, + '3': 1, + 'ALL_QUERIES': 1, + 'UNREAD': 2 + }); + done(); + } + ); + it( + 'should throw in the event of an error', + async (done) => { + await expect(folders.folderCounts({ + user: { id: 1 } + })).rejects.toBeTruthy(); + done(); + } + ); + + }); }); diff --git a/test/resolvers/labels.test.js b/test/resolvers/labels.test.js index 25736d9..cdb3e1c 100644 --- a/test/resolvers/labels.test.js +++ b/test/resolvers/labels.test.js @@ -29,6 +29,21 @@ describe('Labels', () => { done(); }); }); + describe('getLabel', () => { + // Make the call + labels.getLabel({ params: { id: 1 }}); + it('should be called', (done) => { + expect(pool.query).toHaveBeenCalled(); + done(); + }); + it('should be passed correct SQL', (done) => { + expect(pool.query).toBeCalledWith( + 'SELECT * FROM label WHERE id = $1', + [1] + ); + done(); + }); + }); describe('upsertLabel', () => { // Make the call *with an ID* labels.upsertLabel({ @@ -78,4 +93,54 @@ describe('Labels', () => { done(); }); }); + describe('labelCounts', () => { + const mockedFn = jest.fn(() => + new Promise((resolve) => { + return resolve({ + rows: [ + { id: 1, label_id: 2 }, + { id: 2, label_id: 3 }, + { id: 3, label_id: 1 } + ], + rowCount: 3 + }); + }) + ); + it( + 'allLabels, allQueries & allLabelsForQueries should be called', + async (done) => { + await labels.labelCounts( + { user: { id: 1 } }, + mockedFn, + mockedFn, + mockedFn + ); + expect(mockedFn).toHaveBeenCalledTimes(3); + done(); + } + ); + it('should return the correct result', async (done) => { + const result = await labels.labelCounts( + { user: { id: 1 } }, + mockedFn, + mockedFn, + mockedFn + ); + expect(result).toEqual({ + '1': 1, + '2': 1, + '3': 1 + }); + done(); + }); + it( + 'should throw in the event of an error', + async (done) => { + await expect(labels.labelCounts({ + user: { id: 1 } + })).rejects.toBeTruthy(); + done(); + } + ); + }); }); diff --git a/test/resolvers/queries.test.js b/test/resolvers/queries.test.js index bc82583..f689720 100644 --- a/test/resolvers/queries.test.js +++ b/test/resolvers/queries.test.js @@ -30,7 +30,7 @@ describe('Queries', () => { ); done(); }); - // Pass title, offset, limit, folder and label parameters + // Pass title, offset, limit, calculated folder and label parameters it('should be passed correct SQL including parameters', (done) => { queries.allQueries({ query: { @@ -45,11 +45,59 @@ describe('Queries', () => { expect( pool.query ).toBeCalledWith( - "SELECT q.* FROM query q, querylabel ql WHERE q.id = ql.query_id AND ql.label_id = $1 AND folder = $2 AND title ILIKE '%' || $3 || '%' AND initiator = $4 ORDER BY updated_at DESC OFFSET $5 LIMIT $6", + 'SELECT q.* FROM query q, querylabel ql WHERE q.id = ql.query_id AND ql.label_id = $1 AND folder = $2 AND title ILIKE \'%\' || $3 || \'%\' AND initiator = $4 ORDER BY updated_at DESC OFFSET $5 LIMIT $6', [1, 'ESCALATED', 'The Clone Wars', 2, 20, 10] ); done(); }); + // Pass ALL_QUERIES folder parameter + it('should be passed correct SQL (ALL_QUERIES)', (done) => { + queries.allQueries({ + query: { + title: 'The Clone Wars', + folder: 'ALL_QUERIES' + }, + user: {id: 2, role_code: 'CUSTOMER'} + }); + expect( + pool.query + ).toBeCalledWith( + 'SELECT q.* FROM query q, queryuser qu WHERE q.id = qu.query_id AND qu.user_id = $1 AND qu.unseen_count = 0 AND title ILIKE \'%\' || $2 || \'%\' AND initiator = $3 ORDER BY updated_at DESC', + [2, 'The Clone Wars', 2] + ); + done(); + }); + // Pass UNREAD folder parameter + it('should be passed correct SQL (UNREAD)', (done) => { + queries.allQueries({ + query: { + title: 'The Clone Wars', + folder: 'UNREAD' + }, + user: {id: 2, role_code: 'CUSTOMER'} + }); + expect( + pool.query + ).toBeCalledWith( + 'SELECT q.* FROM query q, queryuser qu WHERE q.id = qu.query_id AND qu.user_id = $1 AND qu.unseen_count > 0 AND title ILIKE \'%\' || $2 || \'%\' AND initiator = $3 ORDER BY updated_at DESC', + [2, 'The Clone Wars', 2] + ); + done(); + }); + // No filters + it('should be passed correct SQL (No filters)', (done) => { + queries.allQueries({ + user: { id: 2, role_code: 'STAFF' }, + query: {} + }); + expect( + pool.query + ).toBeCalledWith( + 'SELECT q.* FROM query q ORDER BY updated_at DESC', + [] + ); + done(); + }); }); describe('getQuery', () => { it('should be called', (done) => { @@ -228,4 +276,20 @@ describe('Queries', () => { done(); }); }); + describe('unhandledQueries', () => { + it('should be called', (done) => { + queries.unhandledQueries(); + expect(pool.query).toHaveBeenCalled(); + done(); + }); + it('should be called with the correct SQL', (done) => { + queries.unhandledQueries(); + expect( + pool.query + ).toBeCalledWith( + 'SELECT * FROM query WHERE id IN (SELECT query_id FROM (SELECT query_id, creator_id FROM message GROUP BY query_id, creator_id) AS nested GROUP BY query_id HAVING COUNT(*) = 1)' + ); + done(); + }); + }); }); diff --git a/test/resolvers/querylabel.test.js b/test/resolvers/querylabel.test.js index 9c6c8e5..100f86b 100644 --- a/test/resolvers/querylabel.test.js +++ b/test/resolvers/querylabel.test.js @@ -15,6 +15,22 @@ jest.mock('../../config', () => ({ })); describe('QueryLabels', () => { + describe('allLabelsForQueries', () => { + querylabel.allLabelsForQueries([1, 2, 3]); + it('should be called', (done) => { + expect(pool.query).toHaveBeenCalled(); + done(); + }); + it('should be called with correct parameters', (done) => { + expect( + pool.query + ).toBeCalledWith( + 'SELECT query_id, label_id FROM querylabel WHERE query_id IN ($1,$2,$3)', + [1, 2, 3] + ); + done(); + }); + }); describe('addLabelToQuery', () => { querylabel.addLabelToQuery({ params: { query_id: 1, label_id: 2 } diff --git a/test/resolvers/queryuser.test.js b/test/resolvers/queryuser.test.js index 9d11ec2..72de6a3 100644 --- a/test/resolvers/queryuser.test.js +++ b/test/resolvers/queryuser.test.js @@ -1,25 +1,37 @@ // The thing we're testing const queryuser = require('../../resolvers/queryuser'); +const queries = require('../../resolvers/queries'); // The module that queryuser.js depends on (which we're about to mock) const pool = require('../../config'); // Mock pool jest.mock('../../config', () => ({ - // A mock query function - query: jest.fn(() => - new Promise((resolve) => { - return resolve({ rows: [{ rowCount: 0 }]}); - }) + query: jest.fn( + () => Promise.resolve({ rows: [{ rowCount: 10 }] }) ) })); describe('UserQuery', () => { + beforeEach(() => jest.clearAllMocks()); + describe('allUserQueries', () => { + beforeEach(() => queryuser.allUserQueries()); + it('should be called', (done) => { + expect(pool.query).toHaveBeenCalled(); + done(); + }); + it('should be passed correct SQL', (done) => { + expect(pool.query).toBeCalledWith( + 'SELECT * FROM queryuser' + ); + done(); + }); + }); describe('updateMostRecentSeen', () => { - queryuser.updateMostRecentSeen({ + beforeEach(() => queryuser.updateMostRecentSeen({ params: { query_id: 1, user_id: 2 }, body: { most_recent_seen: 100 } - }); + })); it('should be called', (done) => { expect(pool.query).toHaveBeenCalled(); done(); @@ -34,4 +46,285 @@ describe('UserQuery', () => { done(); }); }); + describe('getMostRecentSeen', () => { + beforeEach(() => queryuser.getMostRecentSeen({ id: 1 })); + it('should be called', (done) => { + expect(pool.query).toHaveBeenCalled(); + done(); + }); + it('should be called with correct parameters', (done) => { + expect( + pool.query + ).toBeCalledWith( + 'SELECT query_id, most_recent_seen FROM queryuser WHERE user_id = $1', + [1] + ); + done(); + }); + }); + describe('getUnseenCounts', () => { + beforeEach(() => queryuser.getUnseenCounts({ + query_id: 1, + user_id: 2, + mostRecentSeen: 100 + })); + it('should be called', (done) => { + expect(pool.query).toHaveBeenCalled(); + done(); + }); + it('should be called with correct parameters', (done) => { + expect( + pool.query + ).toBeCalledWith( + 'SELECT count(*) AS rowcount FROM message WHERE query_id = $1 AND creator_id != $2 AND id > $3', + [1, 2, 100] + ); + done(); + }); + }); + describe('calculateUnseenCount', () => { + beforeEach(() => { + const _getUnseenCounts = jest.fn( + () => Promise.resolve({ rows: [{ rowcount: 99 }] }) + ); + queryuser.calculateUnseenCount({ + query_id: 1, + user_id: 2, + mostRecentSeen: 10, + _getUnseenCounts + }); + }); + it('should be called', (done) => { + expect(pool.query).toHaveBeenCalled(); + done(); + }); + it('should be called with correct parameters', (done) => { + expect( + pool.query + ).toHaveBeenCalledWith( + 'UPDATE queryuser SET unseen_count = $1 WHERE query_id = $2 AND user_id = $3 RETURNING *', + [99, 1, 2] + ); + done(); + }); + }); + describe('upsertQueryUser', () => { + beforeEach(() => { + queryuser.upsertQueryUser({ query_id: 1, user_id: 2 }); + }); + it('should be called', (done) => { + expect(pool.query).toHaveBeenCalled(); + done(); + }); + it('should be called with correct parameters', (done) => { + expect( + pool.query + ).toHaveBeenCalledWith( + 'INSERT INTO queryuser VALUES ($1, $2, NOW(), NOW(), 0, 0, 0) ON CONFLICT ON CONSTRAINT userquery_pkey DO UPDATE SET unseen_count = queryuser.unseen_count + 1 WHERE queryuser.query_id = $3 AND queryuser.user_id = $4', + [1, 2, 1, 2] + ); + done(); + }); + }); + describe('upsertQueryUsers', () => { + beforeEach(() => { + queryuser.upsertQueryUsers({ + query_id: 4, + creator: 2, + _associated: () => Promise.resolve([1, 2, 3]) + }); + }); + it('should be called twice', (done) => { + expect(pool.query).toHaveBeenCalledTimes(2); + done(); + }); + it('should be called with correct parameters', (done) => { + expect( + pool.query + ).toHaveBeenNthCalledWith( + 1, + 'INSERT INTO queryuser VALUES ($1, $2, NOW(), NOW(), 0, 0, 0) ON CONFLICT ON CONSTRAINT userquery_pkey DO UPDATE SET unseen_count = queryuser.unseen_count + 1 WHERE queryuser.query_id = $3 AND queryuser.user_id = $4', + [4, 1, 4, 1] + ); + expect( + pool.query + ).toHaveBeenNthCalledWith( + 2, + 'INSERT INTO queryuser VALUES ($1, $2, NOW(), NOW(), 0, 0, 0) ON CONFLICT ON CONSTRAINT userquery_pkey DO UPDATE SET unseen_count = queryuser.unseen_count + 1 WHERE queryuser.query_id = $3 AND queryuser.user_id = $4', + [4, 3, 4, 3] + ); + done(); + }); + }); + describe('updateMostRecentDigests', () => { + beforeEach(() => { + queryuser.updateMostRecentDigests([ + { + query: { + id: 1, + userId: 2, + highMark: 100 + } + }, + { + query: { + id: 3, + userId: 4, + highMark: 200 + } + } + ]); + }); + it('should be called twice', (done) => { + expect(pool.query).toHaveBeenCalledTimes(2); + done(); + }); + it('should be called with correct parameters', (done) => { + expect( + pool.query + ).toHaveBeenNthCalledWith( + 1, + 'INSERT INTO queryuser VALUES ($4, $5, NOW(), NOW(), $1, $2, $3) ON CONFLICT ON CONSTRAINT userquery_pkey DO UPDATE SET most_recent_seen = $1, unseen_count = $2, most_recent_digest = $3 WHERE queryuser.query_id = $4 AND queryuser.user_id = $5', + [0, 0, 100, 1, 2] + ); + expect( + pool.query + ).toHaveBeenNthCalledWith( + 2, + 'INSERT INTO queryuser VALUES ($4, $5, NOW(), NOW(), $1, $2, $3) ON CONFLICT ON CONSTRAINT userquery_pkey DO UPDATE SET most_recent_seen = $1, unseen_count = $2, most_recent_digest = $3 WHERE queryuser.query_id = $4 AND queryuser.user_id = $5', + [0, 0, 200, 3, 4] + ); + done(); + }); + }); + describe('upsertCounts', () => { + it('should be called with correct parameters', (done) => { + queryuser.upsertCounts({ query_id: 1, user_id: 2 }); + expect( + pool.query + ).toHaveBeenCalledWith( + 'INSERT INTO queryuser VALUES ($4, $5, NOW(), NOW(), $1, $2, $3) ON CONFLICT ON CONSTRAINT userquery_pkey DO UPDATE SET most_recent_seen = $1, unseen_count = $2, most_recent_digest = $3 WHERE queryuser.query_id = $4 AND queryuser.user_id = $5', + [0, 0, 0, 1, 2] + ); + done(); + }); + }); + describe('decrementUnseenCount', () => { + beforeEach(() => { + queryuser.decrementUnseenCount({ query_id: 1, user_id: 2 }); + }); + it('should be called', (done) => { + expect(pool.query).toHaveBeenCalled(); + done(); + }); + it('should be called with correct parameters', (done) => { + expect( + pool.query + ).toHaveBeenCalledWith( + 'UPDATE queryuser SET unseen_count = unseen_count - 1 WHERE query_id = $1 AND user_id = $2', + [1, 2] + ); + done(); + }); + }); + describe('decrementMessageDelete', () => { + const getUsersToDecrement = jest.fn(() => Promise.resolve([1, 2, 3])); + const decrementUnseenCount = jest.fn(); + beforeEach(() => { + queryuser.decrementMessageDelete({ + message: { + query_id: 1, + creator_id: 2, + id: 3 + }, + _getUsersToDecrement: getUsersToDecrement, + _decrementUnseenCount: decrementUnseenCount + }); + }); + it('getUsersToDecrement mock should be called', (done) => { + expect(getUsersToDecrement).toHaveBeenCalled(); + done(); + }); + it('decrementUnseenCount mock should be called 3 times with the correct parameters', (done) => { + expect( + decrementUnseenCount + ).toHaveBeenNthCalledWith(1, { query_id: 1, user_id: 1 }); + expect( + decrementUnseenCount + ).toHaveBeenNthCalledWith(2, { query_id: 1, user_id: 2 }); + expect( + decrementUnseenCount + ).toHaveBeenNthCalledWith(3, { query_id: 1, user_id: 3 }); + done(); + }); + }); + describe('getUsersToDecrement', () => { + it('should return the correct value', async (done) => { + const body = { + rows: [ + { + user_id: 1, + most_recent_seen: 50 + }, + { + user_id: 2, + most_recent_seen: 80 + + }, + { + user_id: 3, + most_recent_seen: 80 + }, + { + user_id: 4, + most_recent_seen: 120 + } + ] + }; + const getParticipantCounts = jest.fn( + () => Promise.resolve(body) + ); + const result1 = await queryuser.getUsersToDecrement({ + query_id: 1, + exclude: [2], + most_recent_seen: 100, + _getParticipantCounts: getParticipantCounts + }); + expect(result1).toEqual([1, 3]); + const result2 = await queryuser.getUsersToDecrement({ + query_id: 1, + most_recent_seen: 100, + _getParticipantCounts: getParticipantCounts + }); + expect(result2).toEqual([1, 2, 3]); + const result3 = await queryuser.getUsersToDecrement({ + query_id: 1, + _getParticipantCounts: getParticipantCounts + }); + expect(result3).toEqual([1, 2, 3, 4]); + done(); + }); + }); + describe('getUserUnseenCounts', () => { + it('should be called with correct parameters', (done) => { + queryuser.getUserUnseenCounts({ query_ids: [1, 2], user_id: 3 }); + expect( + pool.query + ).toHaveBeenCalledWith( + 'SELECT query_id, unseen_count FROM queryuser WHERE query_id IN ($1, $2) AND user_id = $3', + [1, 2, 3] + ); + done(); + }); + it('should be called with correct parameters', (done) => { + queryuser.getUserUnseenCounts({ user_id: 3 }); + expect( + pool.query + ).toHaveBeenCalledWith( + 'SELECT query_id, unseen_count FROM queryuser WHERE user_id = $1', + [3] + ); + done(); + }); + }); }); diff --git a/test/resolvers/users.test.js b/test/resolvers/users.test.js index 6a19d62..67bfdf6 100644 --- a/test/resolvers/users.test.js +++ b/test/resolvers/users.test.js @@ -4,6 +4,7 @@ const users = require('../../resolvers/users'); // The modules that users.js depends on (which we're about to mock) const pool = require('../../config'); const roleResolvers = require('../../resolvers/roles'); +const encryption = require('../../helpers/encryption'); // Mock pool jest.mock('../../config', () => ({ @@ -16,14 +17,9 @@ jest.mock('../../config', () => ({ ) })); -// Mock roleResolvers -jest.mock('../../resolvers/roles', () => ({ - getRoleByCode: jest.fn( - () => - new Promise((resolve) => - resolve({ rowCount: 1, rows: [{ id: 1 }] }) - ) - ) +// Mock encryption +jest.mock('../../helpers/encryption', () => ({ + decrypt: jest.fn(() => 'decrypted') })); describe('Users', () => { @@ -84,6 +80,13 @@ describe('Users', () => { done(); }); }); + describe('getUserEmail', () => { + it('should call decrypt', async (done) => { + await users.getUserEmail(); + expect(encryption.decrypt).toHaveBeenCalled(); + done(); + }); + }); describe('getUserByProvider', () => { it('should be called', (done) => { users.getUserByProvider({ @@ -134,60 +137,162 @@ describe('Users', () => { done(); }); it('should be called as an INSERT when ID is not passed', async (done) => { + const stormy = { + name: 'Stormtrooper', + role_id: 1, + provider: 'Imperial Navy', + provider_id: 'TK-421', + provider_meta: 'Terrible shot', + avatar: 'stormtrooper.jpg' + }; await users.upsertUser({ - body: { - name: 'Stormtrooper', - role_id: 1, - provider: 'Imperial Navy', - provider_id: 'TK-421', - provider_meta: 'Terrible shot', - avatar: 'stormtrooper.jpg' - } + body: stormy, + _getRoleByCode: jest.fn( + () => Promise.resolve({ rowCount: 1, rows: [{ id: 20 }] }) + ) + }); + expect(pool.query).toBeCalledWith( + 'INSERT INTO ems_user (id, name, email, role_id, created_at, updated_at, provider, provider_id, provider_meta, avatar) VALUES (default, $1, $2, $3, NOW(), NOW(), $4, $5, $6, $7) RETURNING *', + [ + stormy.name, + stormy.email, + 20, + stormy.provider, + stormy.provider_id, + stormy.provider_meta, + stormy.avatar + ] + ); + jest.clearAllMocks(); + await users.upsertUser({ + body: stormy, + _getRoleByCode: jest.fn( + () => Promise.resolve({ rowCount: 0, rows: [] }) + ) }); - expect(roleResolvers.getRoleByCode).toBeCalledTimes(1); - expect(pool.query).toBeCalledTimes(1); + expect(pool.query).not.toBeCalled(); done(); }); }); - /* - TODO: - Testing nested calls is much harder than it looks, really need to - revisit this describe('upsertUserByProviderId', () => { - // Mock roleResolvers - jest.mock('../../resolvers/users', () => ({ - upsertUser: jest.fn((sql, params) => - new Promise((resolve) => resolve()) - ), - getUserByProvider: jest.fn((sql, params) => - new Promise( - (resolve) => resolve({rowCount: 1, rows: [{id: 1}]}) - ) - ) - })); - it('should follow the update path', async (done) => { - const result = await users.upsertUserByProviderId({ + let sheev = {}; + let mocks = {}; + beforeEach(() => { + sheev = { + id: 66, + role_id: 99, name: 'Sheev Palpatine', provider: 'Imperial Navy', - providerId: 1 + providerId: 50, + providerMeta: {}, + email: 'sheev@thedarkside.com', + avatar: 'https://i.kym-cdn.com/entries/icons/original/000/019/930/1421657233490.jpg' + }; + mocks = { + upsertUser: jest.fn(() => + new Promise((resolve) => resolve()) + ), + getUserByProviderExist: jest.fn(() => + Promise.resolve({ + rowCount: 1, + rows: [{ + id: sheev.id, + role_id: sheev.role_id, + provider_id: sheev.providerId + }] + }) + ), + getUserByProviderNoExist: jest.fn(() => + Promise.resolve({ + rowCount: 0, + rows: [] + }) + ), + getUserByProviderError: jest.fn(() => Promise.reject('fail')), + encryption: { + encrypt: jest.fn((passed) => Promise.resolve(passed)) + } + }; + }); + it('should not call encrypt if no email passed', async (done) => { + const incognitoSheev = { + ...sheev, + email: null + }; + await users.upsertUserByProviderId({ + ...incognitoSheev, + _getUserByProvider: mocks.getUserByProviderExist, + _encryption: mocks.encryption, + _upsertUser: mocks.upsertUser }); - expect(users.upsertUser).toBeCalled(); + expect(mocks.encryption.encrypt).not.toBeCalled(); done(); }); - it('should follow the create path', async (done) => { - pool.query.mockImplementationOnce(() => - new Promise((resolve) => resolve({ rowCount: 0, rows: [] }))); + it('should follow the update path', async (done) => { await users.upsertUserByProviderId({ - name: 'Sheev Palpatine', - provider: 'Imperial Navy', - providerId: 1 + ...sheev, + _getUserByProvider: mocks.getUserByProviderExist, + _encryption: mocks.encryption, + _upsertUser: mocks.upsertUser + }); + expect(mocks.encryption.encrypt).toBeCalledWith(sheev.email); + expect(mocks.getUserByProviderExist).toBeCalledWith({ + params: { + provider: sheev.provider, + providerId: sheev.providerId + } + }); + expect(mocks.upsertUser).toBeCalledWith({ + params: { + id: sheev.id + }, + body: { + name: sheev.name, + email: sheev.email, + role_id: sheev.role_id, + provider_meta: sheev.providerMeta, + avatar: sheev.avatar, + provider_id: sheev.providerId + } + }); + done(); + }); + it('should follow the insert path', async (done) => { + await users.upsertUserByProviderId({ + ...sheev, + _getUserByProvider: mocks.getUserByProviderNoExist, + _encryption: mocks.encryption, + _upsertUser: mocks.upsertUser + }); + expect(mocks.getUserByProviderNoExist).toBeCalledWith({ + params: { + provider: sheev.provider, + providerId: sheev.providerId + } + }); + expect(mocks.upsertUser).toBeCalledWith({ + body: { + name: sheev.name, + email: sheev.email, + provider: sheev.provider, + provider_id: sheev.providerId, + provider_meta: sheev.providerMeta, + avatar: sheev.avatar + } + }); + done(); + }); + it('should catch errors', async (done) => { + const result = await users.upsertUserByProviderId({ + ...sheev, + _getUserByProvider: mocks.getUserByProviderError, + _encryption: mocks.encryption, + _upsertUser: mocks.upsertUser }); - expect(users.getUserByProvider).toBeCalled(); - expect(users.upsertUser).toBeCalledWith(); + expect(result).toEqual('fail'); done(); }); }); - */ describe('deleteUser', () => { it('should be called', (done) => { users.deleteUser({ params: { id: 1 } });