diff --git a/__test__/backend.test.js b/__test__/backend.test.js index 8a23e6833..3bf246846 100644 --- a/__test__/backend.test.js +++ b/__test__/backend.test.js @@ -1,6 +1,6 @@ import { isSqlite } from "../src/server/models/"; import { resolvers } from "../src/server/api/schema"; -import { schema } from "../src/api/schema"; +import schema from "../src/api/schema"; import { assignmentRequiredOrAdminRole } from "../src/server/api/errors"; import { graphql } from "graphql"; @@ -28,7 +28,7 @@ import { createInvite as helperCreateInvite, runGql } from "./test_helpers"; -import { makeExecutableSchema } from "graphql-tools"; +import { makeExecutableSchema } from "@graphql-tools/schema"; import { editUserMutation } from "../src/containers/UserEdit.jsx"; @@ -99,7 +99,12 @@ async function createInvite() { }`; const context = getContext(); try { - const invite = await graphql(mySchema, inviteQuery, rootValue, context); + const invite = await graphql({ + schema: mySchema, + source: inviteQuery, + rootValue, + contextValue: context + }); return invite; } catch (err) { console.error("Error creating invite"); @@ -121,20 +126,20 @@ async function createOrganization(user, name, userId, inviteId) { } }`; - const variables = { + const variableValues = { userId, name, inviteId }; try { - const org = await graphql( - mySchema, - orgQuery, + const org = await graphql({ + schema: mySchema, + source: orgQuery, rootValue, - context, - variables - ); + contextValue: context, + variableValues + }); return org; } catch (err) { console.error("Error creating organization"); @@ -151,7 +156,7 @@ async function createCampaign(user, title, description, organizationId) { title } }`; - const variables = { + const variableValues = { input: { title, description, @@ -160,13 +165,13 @@ async function createCampaign(user, title, description, organizationId) { }; try { - const campaign = await graphql( - mySchema, - campaignQuery, + const campaign = await graphql({ + schema: mySchema, + source: campaignQuery, rootValue, - context, - variables - ); + contextValue: context, + variableValues + }); return campaign; } catch (err) { console.error("Error creating campaign"); @@ -177,14 +182,8 @@ async function createCampaign(user, title, description, organizationId) { // graphQL tests describe("graphql test suite", () => { - beforeAll( - async () => await setupTest(), - global.DATABASE_SETUP_TEARDOWN_TIMEOUT - ); - afterAll( - async () => await cleanupTest(), - global.DATABASE_SETUP_TEARDOWN_TIMEOUT - ); + beforeAll(async () => setupTest(), global.DATABASE_SETUP_TEARDOWN_TIMEOUT); + afterAll(async () => cleanupTest(), global.DATABASE_SETUP_TEARDOWN_TIMEOUT); it("should be undefined when user not logged in", async () => { const query = `{ @@ -193,7 +192,12 @@ describe("graphql test suite", () => { } }`; const context = getContext(); - const result = await graphql(mySchema, query, rootValue, context); + const result = await graphql({ + schema: mySchema, + source: query, + rootValue, + contextValue: context + }); const data = result; expect(typeof data.currentUser).toEqual("undefined"); @@ -207,7 +211,12 @@ describe("graphql test suite", () => { } }`; const context = getContext({ user: testAdminUser }); - const result = await graphql(mySchema, query, rootValue, context); + const result = await graphql({ + schema: mySchema, + source: query, + rootValue, + contextValue: context + }); const { data } = result; expect(data.currentUser.email).toBe("testuser@example.com"); @@ -236,10 +245,10 @@ describe("graphql test suite", () => { expect(testOrganization.data.createOrganization.name).toBe( "Testy test organization" ); - } else { - console.log("Failed to create invite and/or user for organization test"); - return false; + return true; } + console.log("Failed to create invite and/or user for organization test"); + return false; }); it("should create a test campaign", async () => { @@ -257,7 +266,7 @@ describe("graphql test suite", () => { it("should create campaign contacts", async () => { const contact = await createContact(testCampaign.data.createCampaign.id); expect(contact.campaign_id).toBe( - parseInt(testCampaign.data.createCampaign.id) + parseInt(testCampaign.data.createCampaign.id, 10) ); }); @@ -282,7 +291,7 @@ describe("graphql test suite", () => { firstName assignment(campaignId:$campaignId) { contactsCount - needsMessageCount: contactsCount(contactsFilter:{messageStatus:\"needsMessage\"}) + needsMessageCount: contactsCount(contactsFilter:{messageStatus:"needsMessage"}) } } interactionSteps { @@ -302,13 +311,13 @@ describe("graphql test suite", () => { } }`; const context = getContext({ user: testAdminUser }); - const updateCampaign = Object.assign({}, testCampaign.data.createCampaign); + const updateCampaign = { ...testCampaign.data.createCampaign }; const campaignId = updateCampaign.id; testTexterUser = await helperCreateTexter(testOrganization); updateCampaign.texters = [ { - id: testTexterUser.id + id: testTexterUser.id.toString() } ]; delete updateCampaign.id; @@ -317,13 +326,13 @@ describe("graphql test suite", () => { campaignId, campaign: updateCampaign }; - const result = await graphql( - mySchema, - campaignEditQuery, + const result = await graphql({ + schema: mySchema, + source: campaignEditQuery, rootValue, - context, - variables - ); + contextValue: context, + variableValues: variables + }); expect(result.data.editCampaign.texters.length).toBe(1); expect(result.data.editCampaign.texters[0].assignment.contactsCount).toBe( @@ -355,7 +364,6 @@ describe("graphql test suite", () => { describe("contacts", () => { let campaigns; - let contacts; beforeEach(async () => { campaigns = await Promise.all( [ @@ -374,7 +382,7 @@ describe("graphql test suite", () => { ].map(async each => each.save()) ); - contacts = await Promise.all( + await Promise.all( [ new CampaignContact({ campaign_id: campaigns[0].id, @@ -439,7 +447,7 @@ describe("graphql test suite", () => { }); test("resolves unassigned contacts when true", async () => { - const contact = await new CampaignContact({ + await new CampaignContact({ campaign_id: campaign.id, message_status: "needsMessage", cell: "" @@ -473,7 +481,7 @@ describe("graphql test suite", () => { campaign_id: campaign.id }).save(); - const contact = await new CampaignContact({ + await new CampaignContact({ campaign_id: campaign.id, assignment_id: assignment.id, message_status: "closed", @@ -619,24 +627,23 @@ describe("graphql test suite", () => { typeof copiedCampaign.due_by === "number" || typeof copiedCampaign.due_by === "string" ) { - let parsedDate = new Date(copiedCampaign.due_by); + const parsedDate = new Date(copiedCampaign.due_by); expect(parsedDate).toEqual(campaign.due_by); + } else if (isSqlite) { + // Currently an open issue w/ datetime being stored as a string in SQLite3 for Jest tests: https://github.com/TryGhost/node-sqlite3/issues/1355. This results in milliseconds being truncated when getting campaign due_by + const campaignDueBy = campaign.due_by; + + campaignDueBy.setMilliseconds(0); + expect(copiedCampaign.due_by).toEqual(campaignDueBy); } else { - if (isSqlite) { - // Currently an open issue w/ datetime being stored as a string in SQLite3 for Jest tests: https://github.com/TryGhost/node-sqlite3/issues/1355. This results in milliseconds being truncated when getting campaign due_by - const campaignDueBy = campaign.due_by; - - campaignDueBy.setMilliseconds(0); - expect(copiedCampaign.due_by).toEqual(campaignDueBy); - } else { - expect(copiedCampaign.due_by).toEqual(campaign.due_by); - } + expect(copiedCampaign.due_by).toEqual(campaign.due_by); } + if ( typeof copiedCampaign.features === "object" && copiedCampaign.features ) { - let jsonString = JSON.stringify(copiedCampaign.features); + const jsonString = JSON.stringify(copiedCampaign.features); expect(jsonString).toEqual(campaign.features); } else { expect(copiedCampaign.features).toEqual(campaign.features); diff --git a/__test__/components/AssignmentSummary.test.js b/__test__/components/AssignmentSummary.test.js index 1d806e6a2..7f12ca96c 100644 --- a/__test__/components/AssignmentSummary.test.js +++ b/__test__/components/AssignmentSummary.test.js @@ -5,7 +5,7 @@ import React from "react"; import { mount } from "enzyme"; import { StyleSheetTestUtils } from "aphrodite"; import each from "jest-each"; -import { ApolloProvider } from "react-apollo"; +import { ApolloProvider } from "@apollo/client"; import ApolloClientSingleton from "../../src/network/apollo-client-singleton"; import { AssignmentSummaryBase as AssignmentSummary } from "../../src/components/AssignmentSummary"; import Badge from "@material-ui/core/Badge"; diff --git a/__test__/components/IncomingMessageList/ConversationPreviewModal.test.js b/__test__/components/IncomingMessageList/ConversationPreviewModal.test.js index ebf6018ad..883dc3fd4 100644 --- a/__test__/components/IncomingMessageList/ConversationPreviewModal.test.js +++ b/__test__/components/IncomingMessageList/ConversationPreviewModal.test.js @@ -11,7 +11,7 @@ import { prepareDataTableData } from "../../../src/components/IncomingMessageLis import ReactTestUtils from "react-dom/test-utils"; import { createMemoryHistory } from "react-router"; import ApolloClientSingleton from "../../../src/network/apollo-client-singleton"; -import { ApolloProvider } from "react-apollo"; +import { ApolloProvider } from "@apollo/client"; import Dialog from "@material-ui/core/Dialog"; import { r } from "../../../src/server/models"; diff --git a/__test__/components/TopNav.test.js b/__test__/components/TopNav.test.jsx similarity index 100% rename from __test__/components/TopNav.test.js rename to __test__/components/TopNav.test.jsx diff --git a/__test__/extensions/message-handlers/profanity-tagger.test.js b/__test__/extensions/message-handlers/profanity-tagger.test.js index 70533bede..7dd6e62df 100644 --- a/__test__/extensions/message-handlers/profanity-tagger.test.js +++ b/__test__/extensions/message-handlers/profanity-tagger.test.js @@ -9,7 +9,8 @@ import { setupTest, cleanupTest, createStartedCampaign, - sendMessage + sendMessage, + sleep } from "../../test_helpers"; beforeEach(async () => { @@ -62,12 +63,15 @@ describe("Message Hanlder: profanity-tagger", () => { await cacheableData.organization.clear(c.organizationId); // SEND - await sendMessage(c.testContacts[1].id, c.testTexterUser, { - userId: c.testTexterUser.id, + await sendMessage(c.testContacts[1].id.toString(), c.testTexterUser, { + userId: c.testTexterUser.id.toString(), contactNumber: c.testContacts[1].cell, text: "brass shoe eddie homonym", - assignmentId: c.assignmentId + assignmentId: c.assignmentId.toString() }); + + await sleep(5); + // a little stupidly updating messageservice_sid is necessary // because it's not await'd await r @@ -126,17 +130,22 @@ describe("Message Hanlder: profanity-tagger", () => { expect(available(org)).toBeTruthy(); // Confirm texter catch - await sendMessage(c.testContacts[0].id, c.testTexterUser, { - userId: c.testTexterUser.id, + await sendMessage(c.testContacts[0].id.toString(), c.testTexterUser, { + userId: c.testTexterUser.id.toString(), contactNumber: c.testContacts[0].cell, text: "Some fakeslur message", - assignmentId: c.assignmentId + assignmentId: c.assignmentId.toString() }); + + await sleep(10); + const text1 = await r .knex("tag_campaign_contact") .select("tag_id", "campaign_contact_id") - .where("campaign_contact_id", 1); - expect(text1).toEqual([{ tag_id: 2, campaign_contact_id: 1 }]); + .where("campaign_contact_id", c.testContacts[0].id); + expect(text1).toEqual([ + { tag_id: 2, campaign_contact_id: c.testContacts[0].id } + ]); let user = await cacheableData.user.userHasRole( c.testTexterUser, @@ -146,16 +155,19 @@ describe("Message Hanlder: profanity-tagger", () => { expect(user).toBe(true); // Confirm texter no-match - await sendMessage(c.testContacts[1].id, c.testTexterUser, { - userId: c.testTexterUser.id, + await sendMessage(c.testContacts[1].id.toString(), c.testTexterUser, { + userId: c.testTexterUser.id.toString(), contactNumber: c.testContacts[1].cell, text: "brass shoe eddie homonym", - assignmentId: c.assignmentId + assignmentId: c.assignmentId.toString() }); + + await sleep(5); + const text2 = await r .knex("tag_campaign_contact") .select("tag_id", "campaign_contact_id") - .where("campaign_contact_id", 2); + .where("campaign_contact_id", c.testContacts[1].id); expect(text2).toEqual([]); user = await cacheableData.user.userHasRole( @@ -166,12 +178,15 @@ describe("Message Hanlder: profanity-tagger", () => { expect(user).toBe(true); // Confirm texter no-match - await sendMessage(c.testContacts[1].id, c.testTexterUser, { - userId: c.testTexterUser.id, + await sendMessage(c.testContacts[1].id.toString(), c.testTexterUser, { + userId: c.testTexterUser.id.toString(), contactNumber: c.testContacts[1].cell, text: "fakeslur is one too many slurs", - assignmentId: c.assignmentId + assignmentId: c.assignmentId.toString() }); + + await sleep(5); + user = await cacheableData.user.userHasRole( c.testTexterUser, c.organizationId, @@ -209,17 +224,22 @@ describe("Message Hanlder: profanity-tagger", () => { expect(available(org)).toBeTruthy(); // Confirm texter catch - await sendMessage(c.testContacts[0].id, c.testTexterUser, { - userId: c.testTexterUser.id, + await sendMessage(c.testContacts[0].id.toString(), c.testTexterUser, { + userId: c.testTexterUser.id.toString(), contactNumber: c.testContacts[0].cell, text: "Some fakeslur message", - assignmentId: c.assignmentId + assignmentId: c.assignmentId.toString() }); + + await sleep(5); + const text1 = await r .knex("tag_campaign_contact") .select("tag_id", "campaign_contact_id") - .where("campaign_contact_id", 1); - expect(text1).toEqual([{ tag_id: 2, campaign_contact_id: 1 }]); + .where("campaign_contact_id", c.testContacts[0].id); + expect(text1).toEqual([ + { tag_id: 2, campaign_contact_id: c.testContacts[0].id } + ]); const messages = await r.knex("message").select(); expect(messages.length).toBe(1); diff --git a/__test__/lib.test.js b/__test__/lib.test.js index 4800ee67a..10bf193b5 100644 --- a/__test__/lib.test.js +++ b/__test__/lib.test.js @@ -1,8 +1,8 @@ import { resolvers } from "../src/server/api/schema"; -import { schema } from "../src/api/schema"; +import schema from "../src/api/schema"; import twilio from "../src/extensions/service-vendors/twilio"; import { getConfig, hasConfig } from "../src/server/api/lib/config"; -import { makeExecutableSchema } from "graphql-tools"; +import { makeExecutableSchema } from "@graphql-tools/schema"; const mySchema = makeExecutableSchema({ typeDefs: schema, diff --git a/__test__/server/api/campaign/campaign.test.js b/__test__/server/api/campaign/campaign.test.js index 2589c5b19..cce06246d 100644 --- a/__test__/server/api/campaign/campaign.test.js +++ b/__test__/server/api/campaign/campaign.test.js @@ -1,4 +1,4 @@ -import gql from "graphql-tag"; +import { gql } from "@apollo/client"; import { campaignDataQuery as AdminCampaignEditQuery } from "../../../../src/containers/AdminCampaignEdit"; import { bulkReassignCampaignContactsMutation, @@ -237,7 +237,7 @@ it("save campaign interaction steps, edit it, make sure the last value is set", let texterCampaignDataResults = await runGql( campaignQuery, { - assignmentId + assignmentId: assignmentId.toString() }, testTexterUser ); @@ -282,7 +282,7 @@ it("save campaign interaction steps, edit it, make sure the last value is set", texterCampaignDataResults = await runGql( campaignQuery, { - assignmentId + assignmentId: assignmentId.toString() }, testTexterUser ); @@ -363,7 +363,7 @@ it("should save campaign canned responses across copies and match saved data", a testAdminUser ); expect(campaignDataResults.data.campaign.cannedResponses.length).toEqual(6); - for (let i = 0; i < 6; i++) { + for (let i = 0; i < 6; i += 1) { expect(campaignDataResults.data.campaign.cannedResponses[i].title).toEqual( `canned ${i + 1}` ); @@ -382,7 +382,7 @@ it("should save campaign canned responses across copies and match saved data", a testAdminUser ); expect(campaignDataResults.data.campaign.cannedResponses.length).toEqual(6); - for (let i = 0; i < 6; i++) { + for (let i = 0; i < 6; i += 1) { expect(campaignDataResults.data.campaign.cannedResponses[i].title).toEqual( `canned ${i + 1}` ); @@ -397,7 +397,7 @@ it("should save campaign canned responses across copies and match saved data", a ); expect(campaignDataResults.data.campaign.cannedResponses.length).toEqual(6); - for (let i = 0; i < 6; i++) { + for (let i = 0; i < 6; i += 1) { expect(campaignDataResults.data.campaign.cannedResponses[i].title).toEqual( `canned ${i + 1}` ); @@ -415,14 +415,18 @@ describe("Caching", () => { queryLog = []; console.log("STARTING TEXTING"); // eslint-disable-line no-console - for (let i = 0; i < 5; i++) { - await sendMessage(testContacts[i].id, testTexterUser, { - userId: testTexterUser.id, - contactNumber: testContacts[i].cell, - text: "test text", - assignmentId - }); - } + + await Promise.all( + testContacts.slice(0, 5).map(contact => + sendMessage(contact.id, testTexterUser, { + userId: testTexterUser.id, + contactNumber: contact.cell, + text: "test text", + assignmentId: assignmentId.toString() + }) + ) + ); + // should only have done updates and inserts expect( queryLog @@ -465,7 +469,7 @@ describe("Reassignments", () => { isOptedOut: false, validTimezone: true }, - assignmentId, + assignmentId: assignmentId.toString(), organizationId }, testTexterUser @@ -479,14 +483,17 @@ describe("Reassignments", () => { NUMBER_OF_CONTACTS ); // send some texts - for (let i = 0; i < 5; i++) { - await sendMessage(testContacts[i].id, testTexterUser, { - userId: testTexterUser.id, - contactNumber: testContacts[i].cell, - text: "test text", - assignmentId - }); - } + await Promise.all( + testContacts.slice(0, 5).map(contact => + sendMessage(contact.id.toString(), testTexterUser, { + userId: testTexterUser.id.toString(), + contactNumber: contact.cell, + text: "test text", + assignmentId: assignmentId.toString() + }) + ) + ); + // TEXTER 1 (95 needsMessage, 5 needsResponse) texterCampaignDataResults = await runGql( TexterTodoQuery, @@ -496,7 +503,7 @@ describe("Reassignments", () => { isOptedOut: false, validTimezone: true }, - assignmentId, + assignmentId: assignmentId.toString(), organizationId }, testTexterUser @@ -512,11 +519,11 @@ describe("Reassignments", () => { // using editCampaign await assignTexter(testAdminUser, testTexterUser, testCampaign, [ { - id: testTexterUser.id, + id: testTexterUser.id.toString(), needsMessageCount: 70, contactsCount: NUMBER_OF_CONTACTS }, - { id: testTexterUser2.id, needsMessageCount: 20 } + { id: testTexterUser2.id.toString(), needsMessageCount: 20 } ]); // TEXTER 1 (70 needsMessage, 5 messaged) // TEXTER 2 (20 needsMessage) @@ -528,7 +535,7 @@ describe("Reassignments", () => { isOptedOut: false, validTimezone: true }, - assignmentId, + assignmentId: assignmentId.toString(), organizationId }, testTexterUser @@ -555,7 +562,7 @@ describe("Reassignments", () => { validTimezone: true }, organizationId, - assignmentId: assignmentId2 + assignmentId: assignmentId2.toString() }, testTexterUser2 ); @@ -567,19 +574,23 @@ describe("Reassignments", () => { ); const assignmentContacts2 = texterCampaignDataResults.data.assignment.contacts; - for (let i = 0; i < 5; i++) { - const contact = testContacts.filter( - c => assignmentContacts2[i].id === c.id.toString() - )[0]; - const messageRes = await sendMessage(contact.id, testTexterUser2, { - userId: testTexterUser2.id, - contactNumber: contact.cell, - text: "test text autorespond", - assignmentId: assignmentId2 - }); - } + + await Promise.all( + assignmentContacts2.slice(0, 5).map(assignmentContact => { + const contact = testContacts.filter( + c => assignmentContact.id === c.id.toString() + )[0]; + return sendMessage(contact.id.toString(), testTexterUser2, { + userId: testTexterUser2.id.toString(), + contactNumber: contact.cell, + text: "test text autorespond", + assignmentId: assignmentId2.toString() + }); + }) + ); + // does this sleep fix the "sometimes 4 instead of 5" below? - await sleep(5); + await sleep(50); // TEXTER 1 (70 needsMessage, 5 messaged) // TEXTER 2 (15 needsMessage, 5 needsResponse) texterCampaignDataResults = await runGql( @@ -591,7 +602,7 @@ describe("Reassignments", () => { validTimezone: true }, organizationId, - assignmentId: assignmentId2 + assignmentId: assignmentId2.toString() }, testTexterUser2 ); @@ -610,7 +621,7 @@ describe("Reassignments", () => { validTimezone: true }, organizationId, - assignmentId: assignmentId2 + assignmentId: assignmentId2.toString() }, testTexterUser2 ); @@ -623,19 +634,24 @@ describe("Reassignments", () => { ); const makeFilterFunction = contactToMatch => contactToTest => contactToMatch.id === contactToTest.id.toString(); - for (let i = 0; i < 3; i++) { - const contact = testContacts.filter( - makeFilterFunction( - texterCampaignDataResults.data.assignment.contacts[i] - ) - )[0]; - await sendMessage(contact.id, testTexterUser2, { - userId: testTexterUser2.id, - contactNumber: contact.cell, - text: "keep talking", - assignmentId: assignmentId2 - }); - } + + await Promise.all( + texterCampaignDataResults.data.assignment.contacts + .slice(0, 3) + .map(assignmentContact => { + const contact = testContacts.filter( + makeFilterFunction(assignmentContact) + )[0]; + + return sendMessage(contact.id.toString(), testTexterUser2, { + userId: testTexterUser2.id.toString(), + contactNumber: contact.cell, + text: "keep talking", + assignmentId: assignmentId2.toString() + }); + }) + ); + // TEXTER 1 (70 needsMessage, 5 messaged) // TEXTER 2 (15 needsMessage, 2 needsResponse, 3 convo) texterCampaignDataResults = await runGql( @@ -647,7 +663,7 @@ describe("Reassignments", () => { validTimezone: true }, organizationId, - assignmentId: assignmentId2 + assignmentId: assignmentId2.toString() }, testTexterUser2 ); @@ -666,7 +682,7 @@ describe("Reassignments", () => { validTimezone: true }, organizationId, - assignmentId: assignmentId2 + assignmentId: assignmentId2.toString() }, testTexterUser2 ); @@ -677,10 +693,21 @@ describe("Reassignments", () => { 20 ); await assignTexter(testAdminUser, testTexterUser, testCampaign, [ - { id: testTexterUser.id, needsMessageCount: 60, contactsCount: 75 }, + { + id: testTexterUser.id.toString(), + needsMessageCount: 60, + contactsCount: 75 + }, // contactsCount: 30 = 25 (desired needsMessage) + 5 (messaged) - { id: testTexterUser2.id, needsMessageCount: 25, contactsCount: 30 } + { + id: testTexterUser2.id.toString(), + needsMessageCount: 25, + contactsCount: 30 + } ]); + + await sleep(20); + // TEXTER 1 (60 needsMessage, 5 messaged) // TEXTER 2 (25 needsMessage, 2 needsResponse, 3 convo) texterCampaignDataResults = await runGql( @@ -691,7 +718,7 @@ describe("Reassignments", () => { isOptedOut: false, validTimezone: true }, - assignmentId, + assignmentId: assignmentId.toString(), organizationId }, testTexterUser @@ -705,7 +732,7 @@ describe("Reassignments", () => { validTimezone: true }, organizationId, - assignmentId: assignmentId2 + assignmentId: assignmentId2.toString() }, testTexterUser2 ); @@ -728,7 +755,7 @@ describe("Reassignments", () => { reassignCampaignContactsMutation, { organizationId, - newTexterUserId: testTexterUser2.id, + newTexterUserId: testTexterUser2.id.toString(), campaignIdsContactIds: [ { campaignId: testCampaign.id, @@ -741,6 +768,7 @@ describe("Reassignments", () => { }, testAdminUser ); + // TEXTER 1 (60 needsMessage, 4 messaged) // TEXTER 2 (25 needsMessage, 2 needsResponse, 3 convo, 1 messaged) texterCampaignDataResults = await runGql( @@ -751,7 +779,7 @@ describe("Reassignments", () => { isOptedOut: false, validTimezone: true }, - assignmentId, + assignmentId: assignmentId.toString(), organizationId }, testTexterUser @@ -765,7 +793,7 @@ describe("Reassignments", () => { validTimezone: true }, organizationId, - assignmentId: assignmentId2 + assignmentId: assignmentId2.toString() }, testTexterUser2 ); @@ -787,13 +815,13 @@ describe("Reassignments", () => { bulkReassignCampaignContactsMutation, { organizationId, - newTexterUserId: testTexterUser.id, + newTexterUserId: testTexterUser.id.toString(), contactsFilter: { messageStatus: "needsResponse", isOptedOut: false, validTimezone: true }, - campaignsFilter: { campaignId: testCampaign.id }, + campaignsFilter: { campaignId: parseInt(testCampaign.id, 10) }, assignmentsFilter: { texterId: testTexterUser2.id }, messageTextFilter: "" }, @@ -809,7 +837,7 @@ describe("Reassignments", () => { isOptedOut: false, validTimezone: true }, - assignmentId, + assignmentId: assignmentId.toString(), organizationId }, testTexterUser @@ -823,7 +851,7 @@ describe("Reassignments", () => { isOptedOut: false, validTimezone: true }, - assignmentId: assignmentId2, + assignmentId: assignmentId2.toString(), organizationId }, testTexterUser2 @@ -877,7 +905,7 @@ describe("Bulk Send", () => { isOptedOut: false, validTimezone: true }, - assignmentId, + assignmentId: assignmentId.toString(), organizationId }, testTexterUser @@ -904,7 +932,7 @@ describe("Bulk Send", () => { isOptedOut: false, validTimezone: true }, - assignmentId, + assignmentId: assignmentId.toString(), organizationId }, testTexterUser @@ -1046,11 +1074,11 @@ describe("campaigns query", () => { it("correctly filters by a single campaign id", async () => { const campaignsFilter = { - campaignId: testCampaign.id + campaignId: parseInt(testCampaign.id, 10) }; const variables = { cursor, - organizationId, + organizationId: organizationId.toString(), campaignsFilter }; @@ -1061,11 +1089,14 @@ describe("campaigns query", () => { it("correctly filter by more than one campaign id", async () => { const campaignsFilter = { - campaignIds: [testCampaign.id, testCampaign2.id] + campaignIds: [ + parseInt(testCampaign.id, 10), + parseInt(testCampaign2.id, 10) + ] }; const variables = { cursor, - organizationId, + organizationId: organizationId.toString(), campaignsFilter }; @@ -1176,7 +1207,7 @@ describe("all interaction steps fields travel round trip", () => { `; variables = { - assignmentId + assignmentId: assignmentId.toString() }; }); diff --git a/__test__/server/api/campaign/updateQuestionResponses.test.js b/__test__/server/api/campaign/updateQuestionResponses.test.js index 7073188a5..7a86104af 100644 --- a/__test__/server/api/campaign/updateQuestionResponses.test.js +++ b/__test__/server/api/campaign/updateQuestionResponses.test.js @@ -94,7 +94,8 @@ describe("mutations.updateQuestionResponses", () => { // these are the answers to the question "what's your favorite color?" const toReturnColorInteractionSteps = returnedInteractionSteps.filter( interactionStep => - interactionStep.parentInteractionId === returnedInteractionSteps[0].id + interactionStep.parentInteractionId === + returnedInteractionSteps[0].id.toString() ); // this is the interaction step representing the answer "Red" @@ -105,15 +106,16 @@ describe("mutations.updateQuestionResponses", () => { // these are the answers to the question "what's your favorite shade of red" const toReturnShadesOfRedInteractionSteps = returnedInteractionSteps.filter( interactionStep => - interactionStep.parentInteractionId === toReturnRedInteractionStep.id + interactionStep.parentInteractionId === + toReturnRedInteractionStep.id.toString() ); // send initial messages to 2 contacts const promises = contacts.slice(0, numberOfContacts).map(contact => { - return sendMessage(contact.id, texterUser, { + return sendMessage(contact.id.toString(), texterUser, { text: returnedInteractionSteps[0].script, contactNumber: contact.cell, - assignmentId: assignment.id, + assignmentId: assignment.id.toString(), userId: texterUser.id.toString() }); }); @@ -238,13 +240,13 @@ describe("mutations.updateQuestionResponses", () => { questionResponses = [ { - campaignContactId: contacts[0].id, - interactionStepId: interactionSteps[0].id, + campaignContactId: contacts[0].id.toString(), + interactionStepId: interactionSteps[0].id.toString(), value: colorInteractionSteps[0].answerOption }, { - campaignContactId: contacts[0].id, - interactionStepId: redInteractionStep.id, + campaignContactId: contacts[0].id.toString(), + interactionStepId: redInteractionStep.id.toString(), value: shadesOfRedInteractionSteps[0].answerOption } ]; @@ -276,7 +278,7 @@ describe("mutations.updateQuestionResponses", () => { `; const variables = { - ccid: contacts[0].id, + ccid: contacts[0].id.toString(), qr: questionResponses }; @@ -357,8 +359,8 @@ describe("mutations.updateQuestionResponses", () => { `; const getAssignmentVariables = { - assignmentId: assignment.id, - contactIds: contacts[0].id, + assignmentId: assignment.id.toString(), + contactIds: contacts[0].id.toString(), findNew: false }; diff --git a/__test__/server/api/createOptOut.test.js b/__test__/server/api/createOptOut.test.js index 4f302c417..9795fed82 100644 --- a/__test__/server/api/createOptOut.test.js +++ b/__test__/server/api/createOptOut.test.js @@ -26,13 +26,13 @@ describe("createOptOut", () => { optOutContact = startedCampaign.testContacts[20]; optOut = { cell: optOutContact.cell, - assignmentId: startedCampaign.assignmentId, + assignmentId: startedCampaign.assignmentId.toString(), reason: "they were snotty" }; variables = { optOut, - campaignContactId: optOutContact.id + campaignContactId: optOutContact.id.toString() }; }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); diff --git a/__test__/server/api/editOrganization.test.js b/__test__/server/api/editOrganization.test.js index e244cebc6..aec9681df 100644 --- a/__test__/server/api/editOrganization.test.js +++ b/__test__/server/api/editOrganization.test.js @@ -1,9 +1,7 @@ /* eslint-disable no-unused-expressions, consistent-return */ import { r } from "../../../src/server/models/"; import { getFeatures } from "../../../src/server/api/lib/config"; -import { getCampaignsQuery } from "../../../src/containers/AdminCampaignList"; import { editOrganizationGql } from "../../../src/containers/Settings"; -import { GraphQLError } from "graphql/error"; import { cleanupTest, diff --git a/__test__/server/api/errors.test.js b/__test__/server/api/errors.test.js index f2aee7d70..e7eb56b8e 100644 --- a/__test__/server/api/errors.test.js +++ b/__test__/server/api/errors.test.js @@ -40,10 +40,8 @@ describe("errors.js", () => { } expect(error).toBeDefined(); - expect(error.message).toEqual({ - message: "You must login to access that resource.", - status: 401 - }); + expect(error.message).toEqual("You must login to access that resource."); + expect(error.extensions).toMatchObject({ status: 401 }); }); }); diff --git a/__test__/server/api/organization.test.js b/__test__/server/api/organization.test.js index fbdfd4083..0a7f75f98 100644 --- a/__test__/server/api/organization.test.js +++ b/__test__/server/api/organization.test.js @@ -2,7 +2,7 @@ import { isSqlite, r } from "../../../src/server/models/"; import { getCampaignsQuery } from "../../../src/containers/AdminCampaignList"; import { GraphQLError } from "graphql/error"; -import gql from "graphql-tag"; +import { gql } from "@apollo/client"; import * as messagingServices from "../../../src/extensions/service-vendors"; import { @@ -92,7 +92,7 @@ describe("organization", () => { it("filters by a single campaign id", async () => { variables.campaignsFilter = { - campaignId: testCampaign.id + campaignId: parseInt(testCampaign.id, 10) }; const result = await runGql(getCampaignsQuery, variables, testAdminUser); @@ -104,7 +104,10 @@ describe("organization", () => { it("filter by more than one campaign id", async () => { const campaignsFilter = { - campaignIds: [testCampaign.id, testCampaign2.id] + campaignIds: [ + parseInt(testCampaign.id, 10), + parseInt(testCampaign2.id, 10) + ] }; variables.campaignsFilter = campaignsFilter; @@ -235,7 +238,7 @@ describe("organization", () => { organizationQuery = ` query q($organizationId: String!) { organization(id: $organizationId) { - id + id name availableActions { name @@ -314,7 +317,7 @@ describe("organization", () => { } `; - variables = { organizationId: 1 }; + variables = { organizationId: "1" }; jest.spyOn(messagingServices, "fullyConfigured").mockResolvedValue(false); }); diff --git a/__test__/server/api/people.test.js b/__test__/server/api/people.test.js index beeb84700..97f23fafd 100644 --- a/__test__/server/api/people.test.js +++ b/__test__/server/api/people.test.js @@ -6,7 +6,7 @@ import { r } from "../../../src/server/models/"; import { getUsersGql } from "../../../src/containers/PeopleList"; import { GraphQLError } from "graphql/error"; import { resolvers } from "../../../src/server/api/schema"; -import { validate as uuidValidate } from 'uuid'; +import { validate as uuidValidate } from "uuid"; import { setupTest, @@ -62,14 +62,14 @@ describe("people", () => { await updateUserRoles( testAdminUsers[0], organizationId, - testAdminUsers[0].id, + testAdminUsers[0].id.toString(), ["OWNER"] ); await updateUserRoles( testAdminUsers[0], organizationId, - testAdminUsers[1].id, + testAdminUsers[1].id.toString(), ["ADMIN"] ); @@ -158,23 +158,23 @@ describe("people", () => { // assign contacts await assignTexter(testAdminUsers[0], null, testCampaigns[0], [ - { id: testTexterUsers[0].id, needsMessageCount: 3 }, - { id: testTexterUsers[1].id, needsMessageCount: 3 } + { id: testTexterUsers[0].id.toString(), needsMessageCount: 3 }, + { id: testTexterUsers[1].id.toString(), needsMessageCount: 3 } ]); await assignTexter(testAdminUsers[0], null, testCampaigns[1], [ - { id: testTexterUsers[2].id, needsMessageCount: 3 }, - { id: testTexterUsers[3].id, needsMessageCount: 3 } + { id: testTexterUsers[2].id.toString(), needsMessageCount: 3 }, + { id: testTexterUsers[3].id.toString(), needsMessageCount: 3 } ]); await assignTexter(testAdminUsers[0], null, testCampaigns[2], [ - { id: testTexterUsers[0].id, needsMessageCount: 3 }, - { id: testTexterUsers[4].id, needsMessageCount: 3 } + { id: testTexterUsers[0].id.toString(), needsMessageCount: 3 }, + { id: testTexterUsers[4].id.toString(), needsMessageCount: 3 } ]); await assignTexter(testAdminUsers[0], null, testCampaigns[3], [ - { id: testTexterUsers[1].id, needsMessageCount: 3 }, - { id: testTexterUsers[3].id, needsMessageCount: 3 } + { id: testTexterUsers[1].id.toString(), needsMessageCount: 3 }, + { id: testTexterUsers[3].id.toString(), needsMessageCount: 3 } ]); // other stuff @@ -208,7 +208,7 @@ describe("people", () => { it("filters users to those assigned to a single campaign", async () => { const campaignsFilter = { - campaignId: testCampaigns[0].id + campaignId: parseInt(testCampaigns[0].id, 10) }; variables.campaignsFilter = campaignsFilter; const result = await runGql(getUsersGql, variables, testAdminUsers[0]); @@ -217,7 +217,10 @@ describe("people", () => { it("filters users to those assigned to multiple campaigns", async () => { const campaignsFilter = { - campaignIds: [testCampaigns[0].id, testCampaigns[3].id] + campaignIds: [ + parseInt(testCampaigns[0].id, 10), + parseInt(testCampaigns[3].id, 10) + ] }; variables.campaignsFilter = campaignsFilter; const result = await runGql(getUsersGql, variables, testAdminUsers[0]); @@ -334,7 +337,7 @@ describe("people", () => { expect(result.data).toBeUndefined(); expect(result.errors).toEqual([ new GraphQLError( - 'Variable "$filterBy" got invalid value "any"; Expected type FilterPeopleBy; did you mean ANY?' + 'Variable "$filterBy" got invalid value "any"; Value "any" does not exist in "FilterPeopleBy" enum. Did you mean the enum value "ANY"?' ) ]); }); @@ -488,29 +491,37 @@ describe("people", () => { describe("reset password", () => { /** * Run the resetUserPassword mutation - * @param {number} organizationId - * @param {number} texterId - * @param {number} userId + * @param {number} organizationId + * @param {number} texterId + * @param {number} userId * @returns Promise */ function resetUserPassword(admin, organizationId, texterId) { - return resolvers.RootMutation.resetUserPassword(null, { - organizationId: organizationId, - userId: texterId - }, { - loaders: { - organization: { - load: async id => { - return (await r.knex("organization").where({ id }))[0]; - } - } + return resolvers.RootMutation.resetUserPassword( + null, + { + organizationId: organizationId, + userId: texterId.toString() }, - user: admin - }); + { + loaders: { + organization: { + load: async id => { + return (await r.knex("organization").where({ id }))[0]; + } + } + }, + user: admin + } + ); } it("reset local password", () => { - resetUserPassword(testAdminUsers[0], organizationId, testTexterUsers[0].id).then(uuid => { + resetUserPassword( + testAdminUsers[0], + organizationId, + testTexterUsers[0].id + ).then(uuid => { // Non-Auth0 password reset will return verion 4 UUID expect(uuidValidate(uuid)).toBeTruthy(); }); @@ -520,9 +531,15 @@ describe("people", () => { // Remove PASSPORT_STRATEGY env var. PASSPORT_STRATEGY will default to "auth0" if there's nothing explicitly set delete window.PASSPORT_STRATEGY; - resetUserPassword(testAdminUsers[0], organizationId, testTexterUsers[0].id).catch(e => { + resetUserPassword( + testAdminUsers[0], + organizationId, + testTexterUsers[0].id + ).catch(e => { // Auth0 password reset will attempt to make HTTP request, which will fail in Jest test - const match = e.message.match(/Error: Request id (.*) failed; all 2 retries exhausted/); + const match = e.message.match( + /Error: Request id (.*) failed; all 2 retries exhausted/ + ); expect(match).toHaveLength(2); expect(uuidValidate(match[1])).toBeTruthy(); diff --git a/__test__/server/api/updateContactTags.test.js b/__test__/server/api/updateContactTags.test.js index df336e76f..ef0fc57b1 100644 --- a/__test__/server/api/updateContactTags.test.js +++ b/__test__/server/api/updateContactTags.test.js @@ -106,7 +106,7 @@ describe("mutations.updateContactTags", () => { const result = await wrappedMutations.updateContactTags( contactTags, - contacts[0].id + contacts[0].id.toString() ); expect(result.data.updateContactTags).toEqual({ @@ -154,7 +154,7 @@ describe("mutations.updateContactTags", () => { id: tag.id, value: tag.value })), - 999999 // this will cause cacheableData.campaignContact.load to throw an exception + "999999" // this will cause cacheableData.campaignContact.load to throw an exception ); expect(result.errors[0].message).toEqual( diff --git a/__test__/server/texter.test/correctContactsAfterReassignment.test.js b/__test__/server/texter.test/correctContactsAfterReassignment.test.js index face6557b..93dda28f4 100644 --- a/__test__/server/texter.test/correctContactsAfterReassignment.test.js +++ b/__test__/server/texter.test/correctContactsAfterReassignment.test.js @@ -41,7 +41,7 @@ it("should return contacts after they are reassigned", async () => { messageIds: [] }; }), - testTexterUser2.id + testTexterUser2.id.toString() ); const [ @@ -51,10 +51,10 @@ it("should return contacts after they are reassigned", async () => { { messageStatus: "needsMessage", params: { - assignmentId + assignmentId: assignmentId.toString() } }, - testContacts.map(e => e.id), + testContacts.map(e => e.id.toString()), false ); @@ -78,10 +78,10 @@ it("should return contacts after they are reassigned", async () => { { messageStatus: "needsMessage", params: { - assignmentId: newAssignmentId + assignmentId: newAssignmentId.toString() } }, - testContacts.map(e => e.id), + testContacts.map(e => e.id.toString()), false ); diff --git a/__test__/server/texter.test/texter.test.js b/__test__/server/texter.test/texter.test.js index 09f362f17..ad8f19cf4 100644 --- a/__test__/server/texter.test/texter.test.js +++ b/__test__/server/texter.test/texter.test.js @@ -31,6 +31,19 @@ const getContactsQuery = texterTodoOps.queries.contactData.query; const getContactsVars = props => texterTodoOps.queries.contactData.options(props).variables; +function getIndexOfMinId(returnValue) { + const { index: indexOfMinId } = returnValue.data.getAssignmentContacts.reduce( + ({ index, id }, currentValue, currentIndex) => { + if (parseInt(id, 10) > parseInt(currentValue.id, 10)) { + return { index: currentIndex, id: currentValue.id }; + } + return { index, id }; + }, + { index: Number.MAX_SAFE_INTEGER, id: Number.MAX_SAFE_INTEGER } + ); + return indexOfMinId; +} + /* * NOTE: * beforeEach and afterEach are defined in ./common and are run before the tests in the @@ -42,7 +55,7 @@ it("should send an initial message to test contacts", async () => { const organizationId = testOrganization.data.createOrganization.id; const texterTodoProps = { messageStatus: "needsMessage", - params: { assignmentId, organizationId }, + params: { assignmentId: assignmentId.toString(), organizationId }, location: { query: {} } }; @@ -59,13 +72,13 @@ it("should send an initial message to test contacts", async () => { ); const ret2 = await runGql(getAssignmentContacts, assignVars, testTexterUser); - const contact = ret2.data.getAssignmentContacts[0]; + const contact = ret2.data.getAssignmentContacts[getIndexOfMinId(ret2)]; const message = { contactNumber: contact.cell, - userId: testTexterUser.id, + userId: testTexterUser.id.toString(), text: "test text", - assignmentId + assignmentId: assignmentId.toString() }; const [messageMutation, messageVars] = sendMessageMutAndVars( @@ -108,14 +121,16 @@ it("should send an initial message to test contacts", async () => { // Refetch the contacts via gql to check the caching const ret3 = await runGql(getAssignmentContacts, assignVars, testTexterUser); - expect(ret3.data.getAssignmentContacts[0].messageStatus).toEqual("messaged"); + expect( + ret3.data.getAssignmentContacts[getIndexOfMinId(ret3)].messageStatus + ).toEqual("messaged"); }); it("should be able to receive a response and reply (using fakeService)", async () => { const organizationId = testOrganization.data.createOrganization.id; const texterTodoProps = { messageStatus: "needsMessage", - params: { assignmentId, organizationId }, + params: { assignmentId: assignmentId.toString(), organizationId }, location: { query: {} } }; @@ -132,13 +147,13 @@ it("should be able to receive a response and reply (using fakeService)", async ( ); const ret2 = await runGql(getAssignmentContacts, assignVars, testTexterUser); - const contact = ret2.data.getAssignmentContacts[0]; + const contact = ret2.data.getAssignmentContacts[getIndexOfMinId(ret2)]; const message = { contactNumber: contact.cell, - userId: testTexterUser.id, + userId: testTexterUser.id.toString(), text: "test text autorespond", - assignmentId + assignmentId: assignmentId.toString() }; const [messageMutation, messageVars] = sendMessageMutAndVars( @@ -150,7 +165,7 @@ it("should be able to receive a response and reply (using fakeService)", async ( // wait for fakeservice to autorespond await waitForExpect(async () => { - const dbMessage = await r.knex("message"); + const dbMessage = await r.knex("message").orderBy("created_at"); expect(dbMessage.length).toEqual(2); expect(dbMessage[1]).toEqual( expect.objectContaining({ @@ -171,16 +186,16 @@ it("should be able to receive a response and reply (using fakeService)", async ( // Refetch the contacts via gql to check the caching const ret3 = await runGql(getAssignmentContacts, assignVars, testTexterUser); - expect(ret3.data.getAssignmentContacts[0].messageStatus).toEqual( - "needsResponse" - ); + expect( + ret3.data.getAssignmentContacts[getIndexOfMinId(ret3)].messageStatus + ).toEqual("needsResponse"); // Then we reply const message2 = { contactNumber: contact.cell, - userId: testTexterUser.id, + userId: testTexterUser.id.toString(), text: "reply", - assignmentId + assignmentId: assignmentId.toString() }; const [replyMutation, replyVars] = sendMessageMutAndVars( @@ -192,7 +207,7 @@ it("should be able to receive a response and reply (using fakeService)", async ( // wait for fakeservice to mark the message as sent await waitForExpect(async () => { - const dbMessage = await r.knex("message"); + const dbMessage = await r.knex("message").orderBy("created_at"); expect(dbMessage.length).toEqual(3); expect(dbMessage[2]).toEqual( expect.objectContaining({ @@ -205,5 +220,7 @@ it("should be able to receive a response and reply (using fakeService)", async ( // Refetch the contacts via gql to check the caching const ret4 = await runGql(getAssignmentContacts, assignVars, testTexterUser); - expect(ret4.data.getAssignmentContacts[0].messageStatus).toEqual("convo"); + expect( + ret4.data.getAssignmentContacts[getIndexOfMinId(ret4)].messageStatus + ).toEqual("convo"); }); diff --git a/__test__/test_helpers.js b/__test__/test_helpers.js index 377a92a8b..058762cd2 100644 --- a/__test__/test_helpers.js +++ b/__test__/test_helpers.js @@ -1,15 +1,16 @@ -import _ from "lodash"; +import { makeExecutableSchema } from "@graphql-tools/schema"; +import { graphql } from "graphql"; +import apiSchema from "../src/api/schema"; +import { resolvers } from "../src/server/api/schema"; import { + CampaignContact, + User, cacheableData, createLoaders, createTables, dropTables, - User, - CampaignContact, r -} from "../src/server/models/"; -import { graphql } from "graphql"; -import gql from "graphql-tag"; +} from "../src/server/models"; // Cypress integration tests do not use jest but do use these helpers // They would benefit from mocking mail services, though, so something to look in to. @@ -69,7 +70,7 @@ export async function createContacts(campaign, count = 1) { const campaignId = campaign.id; const contacts = []; const startNum = "+15155500000"; - for (let i = 0; i < count; i++) { + for (let i = 0; i < count; i += 1) { const contact = new CampaignContact({ first_name: `Ann${i}`, last_name: `Lewis${i}`, @@ -77,18 +78,18 @@ export async function createContacts(campaign, count = 1) { zip: "12345", campaign_id: campaignId }); + + // await in loop to avoid making too many database connections + // eslint-disable-next-line no-await-in-loop await contact.save(); + contacts.push(contact); } return contacts; } -import { makeExecutableSchema } from "graphql-tools"; -import { resolvers } from "../src/server/api/schema"; -import { schema } from "../src/api/schema"; - -const mySchema = makeExecutableSchema({ - typeDefs: schema, +const schema = makeExecutableSchema({ + typeDefs: apiSchema, resolvers, allowUndefinedInResolve: true }); @@ -101,18 +102,18 @@ function getGqlOperationText(op) { } export async function runGql(operation, vars, user) { - const operationText = getGqlOperationText(operation) || operation; + const source = getGqlOperationText(operation) || operation; const rootValue = {}; - const context = getContext({ user }); - const result = await graphql( - mySchema, - operationText, + const contextValue = getContext({ user }); + const result = await graphql({ + schema, + source, rootValue, - context, - vars - ); + contextValue, + variableValues: vars + }); if (result && result.errors) { - console.log("runGql failed " + JSON.stringify(result)); + console.log(`runGql failed ${JSON.stringify(result)}`); } return result; } @@ -123,15 +124,21 @@ export const updateUserRoles = async ( userId, roles ) => { - const query = `mutation editOrganizationRoles( - $organizationId: String!, - $userId: String!, - $roles: [String]) { - editOrganizationRoles(userId: $userId, organizationId: $organizationId, roles: $roles) { - id + const query = ` + mutation editOrganizationRoles( + $organizationId: String! + $userId: String! + $roles: [String] + ) { + editOrganizationRoles( + userId: $userId + organizationId: $organizationId + roles: $roles + ) { + id + } } - }`; - + `; const variables = { organizationId, userId, @@ -139,51 +146,67 @@ export const updateUserRoles = async ( }; const result = await runGql(query, variables, adminUser); if (result && result.errors) { - throw new Error("editOrganizationRoles failed " + JSON.stringify(result)); + throw new Error(`editOrganizationRoles failed ${JSON.stringify(result)}`); } return result; }; export async function createInvite() { const rootValue = {}; - const inviteQuery = `mutation { - createInvite(invite: {is_valid: true}) { - id + const source = ` + mutation { + createInvite(invite: { is_valid: true }) { + id + } } - }`; - const context = getContext(); - return await graphql(mySchema, inviteQuery, rootValue, context); + `; + const contextValue = getContext(); + return await graphql({ + schema, + source, + rootValue, + contextValue + }); } export async function createOrganization(user, invite) { const rootValue = {}; const name = "Testy test organization"; const userId = user.id; + const inviteId = invite.data.createInvite.id; - const context = getContext({ user }); + const contextValue = getContext({ user }); - const orgQuery = `mutation createOrganization($name: String!, $userId: String!, $inviteId: String!) { - createOrganization(name: $name, userId: $userId, inviteId: $inviteId) { - id - uuid + const source = ` + mutation createOrganization( + $name: String! + $userId: String! + $inviteId: String! + ) { + createOrganization(name: $name, userId: $userId, inviteId: $inviteId) { + id + uuid + } } - }`; + `; - const variables = { - userId, + const variableValues = { + userId: userId.toString(), name, inviteId }; - const result = await graphql( - mySchema, - orgQuery, + + const result = await graphql({ + schema, + source, rootValue, - context, - variables - ); + contextValue, + variableValues + }); + if (result && result.errors) { - throw new Error("createOrganization failed " + JSON.stringify(result)); + throw new Error(`createOrganization failed ${JSON.stringify(result)}`); } return result; } @@ -238,9 +261,9 @@ export async function setTwilioAuth(user, organization) { const twilioMessageServiceSid = "test_message_service"; const orgId = organization.data.createOrganization.id; - const context = getContext({ user }); + const contextValue = getContext({ user }); - const query = ` + const source = ` mutation updateServiceVendorConfig( $organizationId: String! $serviceName: String! @@ -263,15 +286,21 @@ export async function setTwilioAuth(user, organization) { twilioMessageServiceSid }; - const variables = { + const variableValues = { organizationId: orgId, serviceName: "twilio", config: JSON.stringify(twilioConfig) }; - const result = await graphql(mySchema, query, rootValue, context, variables); + const result = await graphql({ + schema, + source, + rootValue, + contextValue, + variableValues + }); if (result && result.errors) { - console.log("updateServiceVendorConfig failed " + JSON.stringify(result)); + console.log(`updateServiceVendorConfig failed ${JSON.stringify(result)}`); } return result; } @@ -285,14 +314,16 @@ export async function createCampaign( const rootValue = {}; const description = "test description"; const organizationId = organization.data.createOrganization.id; - const context = getContext({ user }); + const contextValue = getContext({ user }); - const campaignQuery = `mutation createCampaign($input: CampaignInput!) { - createCampaign(campaign: $input) { - id + const source = ` + mutation createCampaign($input: CampaignInput!) { + createCampaign(campaign: $input) { + id + } } - }`; - const variables = { + `; + const variableValues = { input: { title, description, @@ -300,15 +331,15 @@ export async function createCampaign( ...args } }; - const result = await graphql( - mySchema, - campaignQuery, + const result = await graphql({ + schema, + source, rootValue, - context, - variables - ); + contextValue, + variableValues + }); if (result.errors) { - throw new Error("Create campaign failed " + JSON.stringify(result)); + throw new Error(`Create campaign failed ${JSON.stringify(result)}`); } return result.data.createCampaign; } @@ -322,19 +353,20 @@ export async function saveCampaign( ) { const rootValue = {}; const description = "test description"; - const organizationId = campaign.organizationId; + const { organizationId } = campaign; const campaignId = campaign.id; - const context = getContext({ user }); + const contextValue = getContext({ user }); - const campaignQuery = `mutation editCampaign($campaignId: String!, $campaign: CampaignInput!) { - editCampaign(id: $campaignId, campaign: $campaign) { - id - title - useOwnMessagingService + const source = ` + mutation editCampaign($campaignId: String!, $campaign: CampaignInput!) { + editCampaign(id: $campaignId, campaign: $campaign) { + id + title + useOwnMessagingService + } } - }`; - - const variables = { + `; + const variableValues = { campaign: { title, description, @@ -342,18 +374,19 @@ export async function saveCampaign( }, campaignId }; - const result = await graphql( - mySchema, - campaignQuery, + const result = await graphql({ + schema, + source, rootValue, - context, - variables - ); + contextValue, + variableValues + }); if (result.errors) { - throw new Error("Create campaign failed " + JSON.stringify(result)); + throw new Error(`Create campaign failed ${JSON.stringify(result)}`); } if (useOwnMessagingService !== "false" || inventoryPhoneNumberCounts) { - const serviceManagerQuery = `mutation updateServiceManager( + const serviceManagerQuery = ` + mutation updateServiceManager( $organizationId: String! $campaignId: String! $serviceManagerName: String! @@ -365,28 +398,31 @@ export async function saveCampaign( serviceManagerName: $serviceManagerName updateData: $updateData ) { - id - name - data - fullyConfigured + id + name + data + fullyConfigured + } } - }`; - const managerResult = await graphql( - mySchema, - serviceManagerQuery, + `; + + const serviceManagerVariableValues = { + organizationId, + serviceManagerName: "per-campaign-messageservices", + updateData: { + useOwnMessagingService, + inventoryPhoneNumberCounts + }, + campaignId + }; + const managerResult = await graphql({ + schema, + source: serviceManagerQuery, rootValue, - context, - { - organizationId, - serviceManagerName: "per-campaign-messageservices", - updateData: { - useOwnMessagingService, - inventoryPhoneNumberCounts - }, - campaignId - } - ); - console.log("managerResult", JSON.stringify(managerResult)); + contextValue, + variableValues: serviceManagerVariableValues + }); + console.log(`managerResult ${JSON.stringify(managerResult)}`); } return result.data.editCampaign; @@ -394,13 +430,21 @@ export async function saveCampaign( export async function copyCampaign(campaignId, user) { const rootValue = {}; - const query = `mutation copyCampaign($campaignId: String!) { - copyCampaign(id: $campaignId) { - id + const source = ` + mutation copyCampaign($campaignId: String!) { + copyCampaign(id: $campaignId) { + id + } } - }`; - const context = getContext({ user }); - return await graphql(mySchema, query, rootValue, context, { campaignId }); + `; + const contextValue = getContext({ user }); + return await graphql({ + schema, + source, + rootValue, + contextValue, + variableValues: { campaignId } + }); } export async function createTexter(organization, userInfo = {}) { @@ -418,27 +462,28 @@ export async function createTexter(organization, userInfo = {}) { "TEXTER" ); if (user.errors) { - throw new Error("createUsers failed " + JSON.stringify(user)); + throw new Error(`createUsers failed ${JSON.stringify(user)}`); } - const joinQuery = ` - mutation joinOrganization($organizationUuid: String!) { - joinOrganization(organizationUuid: $organizationUuid) { - id + const source = ` + mutation joinOrganization($organizationUuid: String!) { + joinOrganization(organizationUuid: $organizationUuid) { + id + } } - }`; - const variables = { + `; + const variableValues = { organizationUuid: organization.data.createOrganization.uuid }; - const context = getContext({ user }); - const result = await graphql( - mySchema, - joinQuery, + const contextValue = getContext({ user }); + const result = await graphql({ + schema, + source, rootValue, - context, - variables - ); + contextValue, + variableValues + }); if (result.errors) { - throw new Error("joinOrganization failed " + JSON.stringify(result)); + throw new Error(`joinOrganization failed ${JSON.stringify(result)}`); } return user; } @@ -450,65 +495,73 @@ export async function assignTexter(admin, user, campaign, assignments) { // contactsCount: (messagedCount from texter) + needsMessageCount (above) // If a userId has an existing assignment, then, also include `contactsCount: ` const rootValue = {}; - const campaignEditQuery = ` - mutation editCampaign($campaignId: String!, $campaign: CampaignInput!) { - editCampaign(id: $campaignId, campaign: $campaign) { - id - assignments { + const source = ` + mutation editCampaign($campaignId: String!, $campaign: CampaignInput!) { + editCampaign(id: $campaignId, campaign: $campaign) { id + assignments { + id + } } } - }`; - const context = getContext({ user: admin }); + `; + const contextValue = getContext({ user: admin }); const updateCampaign = Object.assign({}, campaign); const campaignId = updateCampaign.id; updateCampaign.texters = assignments || [ { - id: user.id + id: user.id.toString() } ]; delete updateCampaign.id; delete updateCampaign.contacts; - const variables = { + const variableValues = { campaignId, campaign: updateCampaign }; - const result = await graphql( - mySchema, - campaignEditQuery, + const result = await graphql({ + schema, + source, rootValue, - context, - variables - ); + contextValue, + variableValues + }); if (result.errors) { - throw new Error("assignTexter failed " + JSON.stringify(result)); + throw new Error(`assignTexter failed ${JSON.stringify(result)}`); } return result; } export async function sendMessage(campaignContactId, user, message) { const rootValue = {}; - const query = ` + const source = ` mutation sendMessage($message: MessageInput!, $campaignContactId: String!) { - sendMessage(message: $message, campaignContactId: $campaignContactId) { + sendMessage(message: $message, campaignContactId: $campaignContactId) { + id + messageStatus + messages { id - messageStatus - messages { - id - createdAt - text - isFromContact - } + createdAt + text + isFromContact } - }`; - const context = getContext({ user }); - const variables = { + } + } + `; + const contextValue = getContext({ user }); + const variableValues = { message, campaignContactId }; - const result = await graphql(mySchema, query, rootValue, context, variables); + const result = await graphql({ + schema, + source, + rootValue, + contextValue, + variableValues + }); if (result.errors) { - console.log("sendMessage errors", result); + console.log(`sendMessage errors ${JSON.stringify(result)}`); } return result; } @@ -547,7 +600,7 @@ export function buildScript(steps = 2, choices = 1) { return []; } const rv = [makeStep(step, max)]; - for (let i = 1; i < choices; i++) { + for (let i = 1; i < choices; i += 1) { rv.push(makeStep(step, max, i)); } return rv; @@ -561,7 +614,7 @@ export async function createScript( { interactionSteps, steps = 2, choices = 1, campaignGqlFragment } = {} ) { const rootValue = {}; - const campaignEditQuery = ` + const source = ` mutation editCampaign($campaignId: String!, $campaign: CampaignInput!) { editCampaign(id: $campaignId, campaign: $campaign) { ${campaignGqlFragment || "id"} @@ -574,76 +627,80 @@ export async function createScript( builtInteractionSteps = buildScript(steps, choices); } - const context = getContext({ user: admin }); - const campaignId = campaign.id; - const variables = { + const contextValue = getContext({ user: admin }); + const campaignId = campaign.id.toString(); + const variableValues = { campaignId, campaign: { interactionSteps: interactionSteps || builtInteractionSteps[0] } }; - return await graphql( - mySchema, - campaignEditQuery, + + return await graphql({ + schema, + source, rootValue, - context, - variables - ); + contextValue, + variableValues + }); } export async function createCannedResponses(admin, campaign, cannedResponses) { // cannedResponses: {title, text} const rootValue = {}; - const campaignEditQuery = ` - mutation editCampaign($campaignId: String!, $campaign: CampaignInput!) { - editCampaign(id: $campaignId, campaign: $campaign) { - id + const source = ` + mutation editCampaign($campaignId: String!, $campaign: CampaignInput!) { + editCampaign(id: $campaignId, campaign: $campaign) { + id + } } - }`; - const context = getContext({ user: admin }); + `; + const contextValue = getContext({ user: admin }); const campaignId = campaign.id; - const variables = { + const variableValues = { campaignId, campaign: { cannedResponses } }; - return await graphql( - mySchema, - campaignEditQuery, + return await graphql({ + schema, + source, rootValue, - context, - variables - ); + contextValue, + variableValues + }); } export async function startCampaign(admin, campaign) { const rootValue = {}; - const startCampaignQuery = `mutation startCampaign($campaignId: String!) { - startCampaign(id: $campaignId) { - id + const source = ` + mutation startCampaign($campaignId: String!) { + startCampaign(id: $campaignId) { + id + } } - }`; - const context = getContext({ user: admin }); - const variables = { campaignId: campaign.id }; - return await graphql( - mySchema, - startCampaignQuery, + `; + const contextValue = getContext({ user: admin }); + const variableValues = { campaignId: campaign.id }; + return await graphql({ + schema, + source, rootValue, - context, - variables - ); + contextValue, + variableValues + }); } export async function getCampaignContact(id) { - return await r + return r .knex("campaign_contact") .where({ id }) .first(); } export async function getOptOut(assignmentId, cell) { - return await r + return r .knex("opt_out") .where({ cell, @@ -819,7 +876,7 @@ export const runComponentQueries = async (queries, user, ownProps) => { const resolvedPromises = await Promise.all(promises); const queryResults = {}; - for (let i = 0; i < keys.length; i++) { + for (let i = 0; i < keys.length; i += 1) { const dataKey = Object.keys(resolvedPromises[i].data)[0]; const key = keys[i]; queryResults[key] = { diff --git a/__test__/workers/assign-texters.test.js b/__test__/workers/assign-texters.test.js index 9a72f5e60..a30724997 100644 --- a/__test__/workers/assign-texters.test.js +++ b/__test__/workers/assign-texters.test.js @@ -5,20 +5,13 @@ import { CampaignContact, JobRequest, Organization, - User, - ZipCode + User } from "../../src/server/models"; import { setupTest, cleanupTest } from "../test_helpers"; describe("test texter assignment in dynamic mode", () => { - beforeAll( - async () => await setupTest(), - global.DATABASE_SETUP_TEARDOWN_TIMEOUT - ); - afterAll( - async () => await cleanupTest(), - global.DATABASE_SETUP_TEARDOWN_TIMEOUT - ); + beforeAll(async () => setupTest(), global.DATABASE_SETUP_TEARDOWN_TIMEOUT); + afterAll(async () => cleanupTest(), global.DATABASE_SETUP_TEARDOWN_TIMEOUT); const testOrg = new Organization({ id: "7777777", @@ -62,10 +55,10 @@ describe("test texter assignment in dynamic mode", () => { ]; it("assigns no contacts to texters in dynamic assignment mode", async () => { - const organization = await Organization.save(testOrg); + await Organization.save(testOrg); const campaign = await Campaign.save(testCampaign); - contactInfo.map(contact => { - CampaignContact.save({ cell: contact, campaign_id: campaign.id }); + contactInfo.map(async contact => { + await CampaignContact.save({ cell: contact, campaign_id: campaign.id }); }); texterInfo.map(async texter => { await User.save({ @@ -81,7 +74,7 @@ describe("test texter assignment in dynamic mode", () => { '{"id": "3","texters":[{"id":"1","needsMessageCount":5,"maxContacts":"","contactsCount":0},{"id":"2","needsMessageCount":5,"maxContacts":"0","contactsCount":0}]}'; const job = new JobRequest({ campaign_id: testCampaign.id, - payload: payload, + payload, queue_name: "3:edit_campaign", job_type: "assign_texters" }); @@ -104,8 +97,8 @@ describe("test texter assignment in dynamic mode", () => { .knex("assignment") .where({ campaign_id: testCampaign.id, user_id: 1 }) .select("max_contacts"); - const maxContactsZero = zero[0]["max_contacts"]; - const maxContactsBlank = blank[0]["max_contacts"]; + const maxContactsZero = zero[0].max_contacts; + const maxContactsBlank = blank[0].max_contacts; expect(maxContactsZero).toEqual(0); expect(maxContactsBlank).toEqual(null); }); @@ -115,7 +108,7 @@ describe("test texter assignment in dynamic mode", () => { '{"id": "3","texters":[{"id":"1","needsMessageCount":0,"maxContacts":"10","contactsCount":0},{"id":"2","needsMessageCount":5,"maxContacts":"15","contactsCount":0}]}'; const job = new JobRequest({ campaign_id: testCampaign.id, - payload: payload, + payload, queue_name: "4:edit_campaign", job_type: "assign_texters" }); @@ -128,8 +121,8 @@ describe("test texter assignment in dynamic mode", () => { .knex("assignment") .where({ campaign_id: testCampaign.id, user_id: "2" }) .select("max_contacts"); - const maxContactsTen = ten[0]["max_contacts"]; - const maxContactsFifteen = fifteen[0]["max_contacts"]; + const maxContactsTen = ten[0].max_contacts; + const maxContactsFifteen = fifteen[0].max_contacts; expect(maxContactsTen).toEqual(10); expect(maxContactsFifteen).toEqual(15); }); diff --git a/package.json b/package.json index 480488034..262f800af 100644 --- a/package.json +++ b/package.json @@ -95,13 +95,8 @@ "@date-io/date-fns": "^1.3.13", "@trt2/gsm-charset-utils": "^1.0.13", "aphrodite": "^2.3.1", - "apollo-cache-inmemory": "^1.6.6", - "apollo-client": "^2.6.10", - "apollo-link": "^1.2.14", - "apollo-link-error": "^1.1.13", - "apollo-link-http": "^1.5.17", - "apollo-server-express": "^1.2.0", - "apollo-utilities": "^1.3.4", + "@apollo/client": "^3.8.8", + "@apollo/server": "^4.9.5", "auth0-js": "^9.14.3", "aws-serverless-express": "^3.3.6", "babel-loader": "^9.1.0", @@ -112,17 +107,17 @@ "camelcase-keys": "^4.1.0", "color-difference": "^0.3.4", "cookie-session": "^2.0.0-alpha.1", + "cors": "^2.8.5", "dataloader": "^1.2.0", "dotenv": "^2.0.0", "express": "^4.14.0", "fs": "^0.0.2", "google-libphonenumber": "^3.0.0", "googleapis": "^39.2.0", - "graphql": "^0.13.2", + "graphql": "^16.8.1", "graphql-date": "^1.0.3", - "graphql-tag": "^2.10.3", - "graphql-tools": "^2.8.0", - "graphql-type-json": "^0.1.4", + "@graphql-tools/schema": "^10.0.2", + "graphql-type-json": "^0.3.2", "heroku-ssl-redirect": "^0.1.1", "humps": "^1.1.0", "is-url": "^1.2.2", @@ -222,7 +217,6 @@ "draft-js": "^0.11.7", "material-ui-search-bar": "^1.0.0", "mui-datatables": "^3.7.7", - "react-apollo": "2.5.7", "react-async-script": "^0.6.0", "react-chartjs-2": "^2.11.1", "react-color": "^2.19.3", diff --git a/src/api/campaign.js b/src/api/campaign.js index 4a70ea5d1..58fcf97eb 100644 --- a/src/api/campaign.js +++ b/src/api/campaign.js @@ -1,4 +1,4 @@ -import gql from "graphql-tag"; +import { gql } from "@apollo/client"; // TODO: rename phoneNumbers to messagingServiceNumbers or something like that export const schema = gql` diff --git a/src/api/organization.js b/src/api/organization.js index d7356bc44..a7aadb3f8 100644 --- a/src/api/organization.js +++ b/src/api/organization.js @@ -1,4 +1,4 @@ -import gql from "graphql-tag"; +import { gql } from "@apollo/client"; export const schema = gql` type ActionChoice { diff --git a/src/api/schema.js b/src/api/schema.js index b68f58796..073c2e12e 100644 --- a/src/api/schema.js +++ b/src/api/schema.js @@ -1,4 +1,4 @@ -import gql from "graphql-tag"; +import { gql } from "@apollo/client"; import { schema as userSchema } from "./user"; import { schema as conversationSchema } from "./conversations"; @@ -176,6 +176,12 @@ const rootSchema = gql` value: String } + type Action { + name: String + displayName: String + instructions: String + } + type FoundContact { found: Boolean assignment: Assignment @@ -410,7 +416,7 @@ const rootSchema = gql` } `; -export const schema = [ +export default [ rootSchema, userSchema, "scalar Date", diff --git a/src/api/service.js b/src/api/service.js index e049f22b2..10badd1b6 100644 --- a/src/api/service.js +++ b/src/api/service.js @@ -1,4 +1,4 @@ -import gql from "graphql-tag"; +import { gql } from "@apollo/client"; export const schema = gql` type ServiceVendor { diff --git a/src/api/tag.js b/src/api/tag.js index b23b820d8..efebf9b19 100644 --- a/src/api/tag.js +++ b/src/api/tag.js @@ -1,4 +1,4 @@ -import gql from "graphql-tag"; +import { gql } from "@apollo/client"; export const schema = gql` type Tag { diff --git a/src/api/user.js b/src/api/user.js index 4e29d96c2..96607a491 100644 --- a/src/api/user.js +++ b/src/api/user.js @@ -1,4 +1,6 @@ -export const schema = ` +import { gql } from "@apollo/client"; + +export const schema = gql` type User { id: ID firstName: String diff --git a/src/client/index.jsx b/src/client/index.jsx index c6da70859..a2a264ba7 100644 --- a/src/client/index.jsx +++ b/src/client/index.jsx @@ -4,7 +4,7 @@ import { Router, browserHistory } from "react-router"; import { StyleSheet } from "aphrodite"; import errorCatcher from "./error-catcher"; import makeRoutes from "../routes"; -import { ApolloProvider } from "react-apollo"; +import { ApolloProvider } from "@apollo/client"; import ApolloClientSingleton from "../network/apollo-client-singleton"; import { login, logout } from "./auth-service"; diff --git a/src/components/AdminDashboard.jsx b/src/components/AdminDashboard.jsx index 4b0fe57ea..1ac44317a 100644 --- a/src/components/AdminDashboard.jsx +++ b/src/components/AdminDashboard.jsx @@ -4,7 +4,7 @@ import { StyleSheet, css } from "aphrodite"; import theme from "../styles/theme"; import { hasRole } from "../lib"; import TopNav from "./TopNav"; -import gql from "graphql-tag"; +import { gql } from "@apollo/client"; import { withRouter } from "react-router"; import loadData from "../containers/hoc/load-data"; import AdminNavigation from "../containers/AdminNavigation"; diff --git a/src/components/App.jsx b/src/components/App.jsx index 80b3c6e47..3b84fca86 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -1,7 +1,7 @@ import PropTypes from "prop-types"; import React, { useState } from "react"; import { ThemeProvider } from "@material-ui/core/styles"; -import { createMuiTheme } from "@material-ui/core/styles"; +import { createTheme } from "@material-ui/core/styles"; import CssBaseline from "@material-ui/core/CssBaseline"; import { defaultTheme } from "../styles/mui-theme"; @@ -21,10 +21,10 @@ const formatTheme = newTheme => { const App = ({ children }) => { const [theme, setTheme] = useState(defaultTheme); - let muiTheme = createMuiTheme(defaultTheme); + let muiTheme = createTheme(defaultTheme); try { // if a bad value is saved this will fail. - muiTheme = createMuiTheme(theme); + muiTheme = createTheme(theme); } catch (e) { console.error("failed to create theme", theme); } diff --git a/src/components/AssignmentTexter/Controls.jsx b/src/components/AssignmentTexter/Controls.jsx index fe47ed1bd..ef751cf49 100644 --- a/src/components/AssignmentTexter/Controls.jsx +++ b/src/components/AssignmentTexter/Controls.jsx @@ -2,6 +2,7 @@ import PropTypes from "prop-types"; import React from "react"; import { css } from "aphrodite"; import { compose } from "recompose"; +import cloneDeep from "lodash/cloneDeep"; import Toolbar from "./Toolbar"; import MessageList from "./MessageList"; import Survey from "./Survey"; @@ -125,7 +126,9 @@ export class AssignmentTexterContactControls extends React.Component { let currentInteractionStep = null; if (availableSteps.length > 0) { - currentInteractionStep = availableSteps[availableSteps.length - 1]; + currentInteractionStep = cloneDeep( + availableSteps[availableSteps.length - 1] + ); currentInteractionStep.question.filteredAnswerOptions = currentInteractionStep.question.answerOptions; } @@ -509,8 +512,8 @@ export class AssignmentTexterContactControls extends React.Component { const otherResponsesLink = currentInteractionStep && - currentInteractionStep.question.filteredAnswerOptions.length > 6 && - filteredCannedResponses.length ? ( + currentInteractionStep.question.filteredAnswerOptions.length > 6 && + filteredCannedResponses.length ? (
- 5 && ( + campaign.cannedResponses.length > + 5 && ( { var textLength = global.HIDE_BRANCHED_SCRIPTS ? this.getShortButtonText( - script.title, - cannedResponseScript ? 40 : 13 - ).length + script.title, + cannedResponseScript ? 40 : 13 + ).length : script.title.length; if (joinedLength + 1 + textLength < 80) { @@ -966,7 +969,7 @@ export class AssignmentTexterContactControls extends React.Component {