diff --git a/.env.example b/.env.example index 9564b79b6..a92a1e597 100644 --- a/.env.example +++ b/.env.example @@ -49,6 +49,7 @@ TWILIO_MESSAGE_VALIDITY_PERIOD= DST_REFERENCE_TIMEZONE='US/Eastern' PASSPORT_STRATEGY=local TEXTER_SIDEBOXES=celebration-gif,default-dynamicassignment,default-releasecontacts,contact-reference,tag-contact,freshworks-widget,default-editinitial,take-conversations,hide-media,texter-feedback,contact-notes +DYNAMICASSIGNMENT_BATCHES=finished-replies-tz,vetted-texters,finished-replies OWNER_CONFIGURABLE=ALL NGP_VAN_API_KEY= NGP_VAN_APP_NAME= diff --git a/.github/workflows/cypress-tests.yaml b/.github/workflows/cypress-tests.yaml index 02e8f3273..b33a2a12f 100644 --- a/.github/workflows/cypress-tests.yaml +++ b/.github/workflows/cypress-tests.yaml @@ -24,10 +24,13 @@ jobs: - 5432:5432 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 - name: Cypress run uses: cypress-io/github-action@v4 - env: + env: DEBUG: '@cypress/github-action' NODE_ENV: test PORT: 3001 diff --git a/.github/workflows/jest-tests.yaml b/.github/workflows/jest-tests.yaml index 76f5df1ca..051537a0d 100644 --- a/.github/workflows/jest-tests.yaml +++ b/.github/workflows/jest-tests.yaml @@ -8,7 +8,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [14.x, 15.x, 16.x] + node-version: [14.x, 15.x, 16.x, 18.x, 20.x] services: redis: image: redis @@ -24,11 +24,14 @@ jobs: - 5432:5432 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v1 + - uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -47,6 +50,11 @@ jobs: image: redis ports: - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 postgres: image: postgres:10 env: @@ -57,11 +65,14 @@ jobs: - 5432:5432 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v1 + - uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -80,6 +91,11 @@ jobs: image: redis ports: - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 postgres: image: postgres:10 env: @@ -90,11 +106,14 @@ jobs: - 5432:5432 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v1 + - uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -114,11 +133,15 @@ jobs: ports: - 6379:6379 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v1 + - uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} diff --git a/.nvmrc b/.nvmrc index 94f5f26dc..2dbbe00e6 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16.18.0 +20.11.1 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index aedacdb4c..eb51ab3fe 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -58,7 +58,7 @@ If you see someone who is making an extra effort to ensure our community is welc Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. -Any community member asked to stop unacceptable behavior is expected to comply immediately. If the member fails to comply immediately, the community organizers may take any action they deem appropriate, up to and including [blocking users from collaboration with MoveOn Github repositories](https://help.github.com/en/articles/blocking-a-user-from-your-organization) and/or temporarily banning or permanently expelling the non-complying member from the community without warning (and without refund in the case of a paid event). +Any community member asked to stop unacceptable behavior is expected to comply immediately. If the member fails to comply immediately, the community organizers may take any action they deem appropriate, up to and including [blocking users from collaboration with StateVoices Github repositories](https://help.github.com/en/articles/blocking-a-user-from-your-organization) and/or temporarily banning or permanently expelling the non-complying member from the community without warning (and without refund in the case of a paid event). ## Our Responsibilities diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f88543341..c581c26fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,6 @@ many important organizations! - ProgCode has community guidelines - We have a weekly(ish) working group at [Progressive HackNight](https://progressivehacknight.org) that organizes in the #wg-spoke_p2p_sms_tool channel in the slack - Feel free to [create an issue or comment on an existing issue](https://github.com/StateVoicesNational/Spoke/issues) -- Every time we hear from the outside progressive developer community, we do a little dance. -- We also welcome reaching out on our [MoveOn Spoke interest form](https://act.moveon.org/survey/spoke-project/) with questions, etc. In all forums we affirm the [Progressive Coder Community Guidelines](https://docs.google.com/document/d/1coMHvuGf6x6Qn_73SEhOXi_QaoRBM__3Zj6_5TyrmWs/edit#heading=h.ab96v3qhdgk9) @@ -57,7 +56,7 @@ Welcome to the project! Once you've completed that first contribution, there are - We use our [Area labels](docs/EXPLANATION-labels.md) to categorize issues into which code feature areas they belong in. Feel free to sort by an area you're interested in. - We use our [Organization labels](docs/EXPLANATION-labels.md) to categorize issues by which organizations are prioritizing them. You can sort by your favorite org and help out there. - More broadly, there are all sort of different [labels](https://github.com/StateVoicesNational/Spoke/labels) we use to filter the issues down and you can leverage that to help you find work. -- Lastly, you can always ping a project maintainer (@ibrand, and @schuyler1d) to get a read on what's on our radar right now. +- Lastly, you can always ping a project maintainer (@mau11 and @engelhartrueben) to get a read on what's on our radar right now. ### Claiming issues - Like for your first issue, comment on the issue and tell us that you're working on it. Feel free to ask any clarifying questions that you have. @@ -83,7 +82,7 @@ The actual process: - We create a new stage-main branch at least twice a month: - The stage-main branch includes the latest approved pull requests in one merged branch - This ensures that PRs will not contain anything that breaks deployment and also will allow us to see if any PRs negatively interact with each other before they end up merged to main. Why bother with this step? It's helpful to have a separate "release candidate" on the stage-main branch because in earlier testing rounds people weren't sure what had been deployed to staging and having a separate branch makes this explicit and clear. - - After stage-main is created, we deploy it to MoveOn's staging instance. We have a small set of QA volunteers who then run through a list of [QA steps](https://github.com/StateVoicesNational/Spoke/blob/main/docs/HOWTO_QA_GUIDE.md) in order to find bugs and test new features. + - After stage-main is created, we deploy it to StateVoices staging instance. We have a small set of QA volunteers who then run through a list of [QA steps](https://github.com/StateVoicesNational/Spoke/blob/main/docs/HOWTO_QA_GUIDE.md) in order to find bugs and test new features. - After QA is completed, and volunteers haven't identified any bugs, we deploy stage-main to production. - We let stage-main run in production for at least a day, before merging stage-main into the main branch. - We never roll code directly to prod without first testing on staging. diff --git a/Dockerfile b/Dockerfile index 9eac37ad5..f5553a187 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -ARG BUILDER_IMAGE=node:16.18 -ARG RUNTIME_IMAGE=node:16.18-alpine +ARG BUILDER_IMAGE=node:20.11.1 +ARG RUNTIME_IMAGE=node:20.11.1-alpine ARG PHONE_NUMBER_COUNTRY=US FROM ${BUILDER_IMAGE} as builder diff --git a/README.md b/README.md index 8b1a48417..c77047630 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,14 @@ [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v1.4%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md) -# StateVoices is the new community steward for Spoke! - -On November 19th, the repo Spoke was transfered from MoveOn to StateVoices. - -[Join us for the Merge Party for more information.](https://www.mobilize.us/statevoices/event/592881/) - ## Spoke History Spoke is an open source text-distribution tool for organizations to mobilize supporters and members into action. Spoke allows you to upload phone numbers, customize scripts and assign volunteers to communicate with supporters while allowing organizations to manage the process. -Spoke was created by Saikat Chakrabarti and Sheena Pakanati, and is now maintained by MoveOn.org. +Spoke was created by Saikat Chakrabarti and Sheena Pakanati. -The latest version is [13.1.0](https://github.com/StateVoicesNational/Spoke/tree/13.1.0) (see [release notes](https://github.com/StateVoicesNational/Spoke/blob/main/docs/RELEASE_NOTES.md#v1310)) +On November 19th, 2023, the repo Spoke was transfered from MoveOn to StateVoices. + +The latest version is [14.0.0](https://github.com/StateVoicesNational/Spoke/tree/14.0.0) (see [release notes](https://github.com/StateVoicesNational/Spoke/blob/main/docs/RELEASE_NOTES.md#v1310)) ## Setting up Spoke @@ -29,7 +25,7 @@ Want to know more? ### Quick Start with Heroku This version of Spoke suitable for testing and, potentially, for small campaigns. This won't cost any money and will not support production(aka large-scale) usage. It's a great way to practice deploying Spoke or see it in action. - + Deploy @@ -39,8 +35,6 @@ Follow up instructions located [here](/docs/HOWTO_HEROKU_DEPLOY.md). **NOTE:** You can upgrade this deployment later for use in a production setting, but keep in mind you will need to migrate data from any prior campaigns. Thus it is best to upgrade before you start any live campaigns. This will cost ~$75 ($25 dyno + $50 postgres) a month and should be suitable for production level usage for most organizations. We recommend that if you plan to use Spoke at scale that you use [this link to deploy with a production infrastructure from the start!](https://heroku.com/deploy?template=https://github.com/StateVoicesNational/Spoke/tree/heroku-button-paid) -Please let us know if you deployed by filling out this form [here](https://act.moveon.org/survey/tech/) - ### Other Options for Production Use diff --git a/__test__/backend.test.js b/__test__/backend.test.js index 8a23e6833..4b0f9cdd1 100644 --- a/__test__/backend.test.js +++ b/__test__/backend.test.js @@ -1,8 +1,7 @@ +import { graphql } from "graphql"; import { isSqlite } from "../src/server/models/"; import { resolvers } from "../src/server/api/schema"; -import { schema } from "../src/api/schema"; -import { assignmentRequiredOrAdminRole } from "../src/server/api/errors"; -import { graphql } from "graphql"; +import { schema as apiSchema } from "../src/api/schema"; console.log("This is an intentional error"); @@ -28,12 +27,11 @@ 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"; -const mySchema = makeExecutableSchema({ - typeDefs: schema, +const schema = makeExecutableSchema({ + typeDefs: apiSchema, resolvers, allowUndefinedInResolve: true }); @@ -97,9 +95,14 @@ async function createInvite() { id } }`; - const context = getContext(); + const contextValue = getContext(); try { - const invite = await graphql(mySchema, inviteQuery, rootValue, context); + const invite = await graphql({ + schema, + source: inviteQuery, + rootValue, + contextValue + }); return invite; } catch (err) { console.error("Error creating invite"); @@ -108,7 +111,7 @@ async function createInvite() { } async function createOrganization(user, name, userId, inviteId) { - 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) { @@ -121,20 +124,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, + source: orgQuery, rootValue, - context, - variables - ); + contextValue, + variableValues + }); return org; } catch (err) { console.error("Error creating organization"); @@ -143,7 +146,7 @@ async function createOrganization(user, name, userId, inviteId) { } async function createCampaign(user, title, description, organizationId) { - const context = getContext({ user }); + const contextValue = getContext({ user }); const campaignQuery = `mutation createCampaign($input: CampaignInput!) { createCampaign(campaign: $input) { @@ -151,7 +154,7 @@ async function createCampaign(user, title, description, organizationId) { title } }`; - const variables = { + const variableValues = { input: { title, description, @@ -160,13 +163,13 @@ async function createCampaign(user, title, description, organizationId) { }; try { - const campaign = await graphql( - mySchema, - campaignQuery, + const campaign = await graphql({ + schema, + source: campaignQuery, rootValue, - context, - variables - ); + contextValue, + variableValues + }); return campaign; } catch (err) { console.error("Error creating campaign"); @@ -192,8 +195,13 @@ describe("graphql test suite", () => { id } }`; - const context = getContext(); - const result = await graphql(mySchema, query, rootValue, context); + const contextValue = getContext(); + const result = await graphql({ + schema, + source: query, + rootValue, + contextValue + }); const data = result; expect(typeof data.currentUser).toEqual("undefined"); @@ -206,8 +214,13 @@ describe("graphql test suite", () => { email } }`; - const context = getContext({ user: testAdminUser }); - const result = await graphql(mySchema, query, rootValue, context); + const contextValue = getContext({ user: testAdminUser }); + const result = await graphql({ + schema, + source: query, + rootValue, + contextValue + }); const { data } = result; expect(data.currentUser.email).toBe("testuser@example.com"); @@ -301,29 +314,29 @@ describe("graphql test suite", () => { } } }`; - const context = getContext({ user: testAdminUser }); + const contextValue = getContext({ user: testAdminUser }); const updateCampaign = Object.assign({}, testCampaign.data.createCampaign); const campaignId = updateCampaign.id; testTexterUser = await helperCreateTexter(testOrganization); updateCampaign.texters = [ { - id: testTexterUser.id + id: testTexterUser.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: campaignEditQuery, rootValue, - context, - variables - ); + contextValue, + variableValues + }); expect(result.data.editCampaign.texters.length).toBe(1); expect(result.data.editCampaign.texters[0].assignment.contactsCount).toBe( @@ -522,6 +535,10 @@ describe("graphql test suite", () => { organization_id: organization.id, role: "OWNER" }).save(); + + const now = new Date(); + const dueBy = isSqlite ? now.getTime() : now; + // creating campaign, interactions (two levels down), and canned responses campaign = await new Campaign({ organization_id: organization.id, @@ -529,7 +546,7 @@ describe("graphql test suite", () => { description: "This is my new campaign", is_started: false, is_archived: false, - due_by: new Date(), + due_by: dueBy, features: JSON.stringify({ MY_FEATURE: "value 1" }), intro_html: "

This is my intro HTML.

", primary_color: "#112233", @@ -619,24 +636,19 @@ 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) { + expect(copiedCampaign.due_by.getTime()).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 { - 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); @@ -769,7 +781,6 @@ describe("editUser mutation", () => { afterEach(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); it("returns the user if it is called with a userId by no userData", async () => { 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..4b7f1f6c2 100644 --- a/__test__/components/IncomingMessageList/ConversationPreviewModal.test.js +++ b/__test__/components/IncomingMessageList/ConversationPreviewModal.test.js @@ -3,15 +3,12 @@ */ import React from "react"; import { mount } from "enzyme"; -import ConversationPreviewModal, { - InnerConversationPreviewModal -} from "../../../src/components/IncomingMessageList/ConversationPreviewModal"; +import { InnerConversationPreviewModal } from "../../../src/components/IncomingMessageList/ConversationPreviewModal"; import { prepareDataTableData } from "../../../src/components/IncomingMessageList"; 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"; @@ -24,6 +21,7 @@ import { createStartedCampaign } from "../../test_helpers"; +// Skipped because muiTheme is not set when called describe.skip("ConversationPreviewModal", () => { let startedCampaign; let optOutContact; @@ -77,7 +75,6 @@ describe.skip("ConversationPreviewModal", () => { afterAll(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); describe("when a message review user opts out a user", () => { diff --git a/__test__/containers/AssignmentTexterContact.test.js b/__test__/containers/AssignmentTexterContact.test.js index a1ca4199b..a563c52ae 100644 --- a/__test__/containers/AssignmentTexterContact.test.js +++ b/__test__/containers/AssignmentTexterContact.test.js @@ -9,13 +9,11 @@ import { AssignmentTexterContact } from "../../src/containers/AssignmentTexterCo import { muiTheme } from "../test_helpers"; import ThemeContext from "../../src/containers/context/ThemeContext"; -var MockDate = require("mockdate"); +import * as timezones from "../../src/lib/timezones"; -jest.mock("../../src/lib/timezones"); -jest.unmock("../../src/lib/tz-helpers"); -jest.useFakeTimers(); +const MockDate = require("mockdate"); -var timezones = require("../../src/lib/timezones"); +jest.useFakeTimers(); const campaign = { id: 9, @@ -110,8 +108,8 @@ describe("when contact is not within texting hours...", () => { }); it("it refreshes data in componentDidMount", () => { - timezones.isBetweenTextingHours.mockReturnValue(false); - timezones.getLocalTime.mockReturnValue( + jest.spyOn(timezones, "isBetweenTextingHours").mockReturnValue(false); + jest.spyOn(timezones, "getLocalTime").mockReturnValue( moment() .utc() .utcOffset(-5) @@ -140,16 +138,17 @@ describe("when contact is not within texting hours...", () => { }); describe("when contact is within texting hours...", () => { - var component; beforeEach(() => { - timezones.isBetweenTextingHours.mockReturnValue(true); - timezones.getLocalTime.mockReturnValue( + jest.spyOn(timezones, "isBetweenTextingHours").mockReturnValue(true); + + jest.spyOn(timezones, "getLocalTime").mockReturnValue( moment() .utc() .utcOffset(-5) ); + StyleSheetTestUtils.suppressStyleInjection(); - component = mount( + mount( { describe("AssignmentTextContact has the proper enabled/disabled state when created", () => { it("is enabled if the contact is inside texting hours", () => { - timezones.isBetweenTextingHours.mockReturnValueOnce(true); - var assignmentTexterContact = new AssignmentTexterContact( + jest.spyOn(timezones, "isBetweenTextingHours").mockReturnValue(true); + const assignmentTexterContact = new AssignmentTexterContact( propsWithEnforcedTextingHoursCampaign ); expect(assignmentTexterContact.state.disabled).toBeFalsy(); @@ -187,8 +186,8 @@ describe("AssignmentTextContact has the proper enabled/disabled state when creat }); it("is disabled if the contact is inside texting hours", () => { - timezones.isBetweenTextingHours.mockReturnValueOnce(false); - var assignmentTexterContact = new AssignmentTexterContact( + jest.spyOn(timezones, "isBetweenTextingHours").mockReturnValue(false); + const assignmentTexterContact = new AssignmentTexterContact( propsWithEnforcedTextingHoursCampaign ); expect(assignmentTexterContact.state.disabled).toBeTruthy(); @@ -199,15 +198,19 @@ describe("AssignmentTextContact has the proper enabled/disabled state when creat }); describe("test isContactBetweenTextingHours", () => { - var assignmentTexterContact; - + let assignmentTexterContact; beforeAll(() => { assignmentTexterContact = new AssignmentTexterContact( propsWithEnforcedTextingHoursCampaign ); - timezones.isBetweenTextingHours.mockImplementation((o, c) => false); + + jest + .spyOn(timezones, "isBetweenTextingHours") + .mockImplementation((o, c) => false); + MockDate.set("2018-02-01T15:00:00.000Z"); - timezones.getLocalTime.mockReturnValue( + + jest.spyOn(timezones, "getLocalTime").mockReturnValue( moment() .utc() .utcOffset(-5) @@ -223,7 +226,7 @@ describe("test isContactBetweenTextingHours", () => { }); it("works when the contact has location data with empty timezone", () => { - let contact = { + const contact = { location: { city: "New York", state: "NY", @@ -239,8 +242,8 @@ describe("test isContactBetweenTextingHours", () => { ).toBeFalsy(); expect(timezones.isBetweenTextingHours.mock.calls).toHaveLength(1); - let theCall = timezones.isBetweenTextingHours.mock.calls[0]; - expect(theCall[0]).toBeFalsy(); + const theCall = timezones.isBetweenTextingHours.mock.calls[0]; + expect(theCall[0]).toEqual({ offset: -5, hasDST: true }); expect(theCall[1]).toEqual({ textingHoursStart: 8, textingHoursEnd: 21, @@ -249,7 +252,7 @@ describe("test isContactBetweenTextingHours", () => { }); it("works when the contact has location data", () => { - let contact = { + const contact = { location: { city: "New York", state: "NY", @@ -265,7 +268,7 @@ describe("test isContactBetweenTextingHours", () => { ).toBeFalsy(); expect(timezones.isBetweenTextingHours.mock.calls).toHaveLength(1); - let theCall = timezones.isBetweenTextingHours.mock.calls[0]; + const theCall = timezones.isBetweenTextingHours.mock.calls[0]; expect(theCall[0]).toEqual({ hasDST: true, offset: -5 }); expect(theCall[1]).toEqual({ textingHoursStart: 8, @@ -275,14 +278,14 @@ describe("test isContactBetweenTextingHours", () => { }); it("works when the contact does not have location data", () => { - let contact = {}; + const contact = {}; expect( assignmentTexterContact.isContactBetweenTextingHours(contact) ).toBeFalsy(); expect(timezones.isBetweenTextingHours.mock.calls).toHaveLength(1); - let theCall = timezones.isBetweenTextingHours.mock.calls[0]; + const theCall = timezones.isBetweenTextingHours.mock.calls[0]; expect(theCall[0]).toBeNull(); expect(theCall[1]).toEqual({ textingHoursStart: 8, diff --git a/__test__/containers/CampaignList.test.js b/__test__/containers/CampaignList.test.js index f434076e1..a81702f13 100644 --- a/__test__/containers/CampaignList.test.js +++ b/__test__/containers/CampaignList.test.js @@ -43,7 +43,7 @@ describe("CampaignList", () => { campaigns: { campaigns: [campaignWithoutCreator], pageInfo: { - limit: 1000, + limit: 100, offset: 0, total: 1 } @@ -86,7 +86,7 @@ describe("CampaignList", () => { campaigns: { campaigns: [campaignWithCreator], pageInfo: { - limit: 1000, + limit: 100, offset: 0, total: 1 } @@ -127,7 +127,7 @@ describe("CampaignList", () => { campaigns: { campaigns: [campaignWithCreator], pageInfo: { - limit: 1000, + limit: 100, offset: 0, total: 1 } diff --git a/__test__/cypress/integration/basic-campaign-e2e.test.js b/__test__/cypress/integration/basic-campaign-e2e.test.js index d96366e41..902099858 100644 --- a/__test__/cypress/integration/basic-campaign-e2e.test.js +++ b/__test__/cypress/integration/basic-campaign-e2e.test.js @@ -101,7 +101,7 @@ describe("End-to-end campaign flow", () => { cy.get("[data-test=editorInteraction] textarea[name=script]").click(); cy.wait(400); cy.get(".DraftEditor-root").type( - "Hi {{}firstName{}} this is {{}texterFirstName{}}, how are you?" + "Hi {{}firstName{}} this is {{}texterAliasOrFirstName{}}, how are you?" ); cy.get("button[data-test=scriptDone]").click(); cy.get("[data-test=questionText] input").type("How are you?"); @@ -127,6 +127,7 @@ describe("End-to-end campaign flow", () => { cy.visit("/app"); const cardSelector = `div[data-test=assignmentSummary-${campaignId}]`; cy.get(cardSelector) + // Checking to see if the campaign title saved as "Integration Test Campaign" .contains(campaignTitle) .should("exist"); cy.get(cardSelector) diff --git a/__test__/cypress/integration/phone-inventory.test.js b/__test__/cypress/integration/phone-inventory.test.js index 243e6247f..aa176c589 100644 --- a/__test__/cypress/integration/phone-inventory.test.js +++ b/__test__/cypress/integration/phone-inventory.test.js @@ -34,7 +34,9 @@ describe("Phone number management screen in the Admin interface", () => { // Waits until job run completes cy.waitUntil( () => - cy.get(`tr:contains(${testAreaCode}) td:nth-child(4)`).contains("1"), + cy + .get(`tr:contains(${testAreaCode}) td:nth-child(4) div`) + .contains("1"), { timeout: 1000 } ); }); diff --git a/__test__/extensions/action-handlers/action-network.test.js b/__test__/extensions/action-handlers/action-network.test.js index 7eeccf79c..7c13fa52d 100644 --- a/__test__/extensions/action-handlers/action-network.test.js +++ b/__test__/extensions/action-handlers/action-network.test.js @@ -2,6 +2,10 @@ import nock from "nock"; import moment from "moment"; const ActionNetwork = require("../../../src/extensions/action-handlers/action-network"); +afterEach(async () => { + await nock.cleanAll(); +}); + expect.extend({ stringifiedObjectEqualObject(receivedString, expectedObject) { let pass = true; diff --git a/__test__/extensions/action-handlers/index.test.js b/__test__/extensions/action-handlers/index.test.js index 62706c07d..b7cf9e1ab 100644 --- a/__test__/extensions/action-handlers/index.test.js +++ b/__test__/extensions/action-handlers/index.test.js @@ -3,13 +3,14 @@ import each from "jest-each"; import { setupTest, cleanupTest, - createStartedCampaign + createStartedCampaign, + flushRedis } from "../../test_helpers"; const ActionHandlers = require("../../../src/extensions/action-handlers"); const uuidv4 = require("uuid").v4; const TestAction = require("../../../src/extensions/action-handlers/test-action"); const ComplexTestAction = require("../../../src/extensions/action-handlers/complex-test-action"); -const log = require("../../../src/lib").log; +const { log } = require("../../../src/lib"); describe("action-handlers/index", () => { let organization; @@ -36,12 +37,11 @@ describe("action-handlers/index", () => { beforeEach(async () => { jest.restoreAllMocks(); - if (r.redis) { - r.redis.flushdb(); - } }); afterEach(async () => { + jest.restoreAllMocks(); + await flushRedis(); const toReset = ["CACHE_PREFIX", "ACTION_HANDLERS"]; toReset.forEach(thingToReset => { @@ -696,7 +696,7 @@ describe("action-handlers/index", () => { describe("when the items property is not an array", () => { beforeEach(async () => { - fakeAction.getClientChoiceData = () => ({ + fakeAction.getClientChoiceData = async () => ({ data: JSON.stringify({ items: {} }), expiresSeconds: 77 }); diff --git a/__test__/extensions/action-handlers/ngpvan-action.test.js b/__test__/extensions/action-handlers/ngpvan-action.test.js index 4f5e5fbaf..cb6f013c5 100644 --- a/__test__/extensions/action-handlers/ngpvan-action.test.js +++ b/__test__/extensions/action-handlers/ngpvan-action.test.js @@ -9,6 +9,7 @@ require("../../test_helpers"); afterEach(async () => { jest.restoreAllMocks(); + await nock.cleanAll(); }); describe("ngpvn-action", () => { @@ -1071,7 +1072,7 @@ describe("ngpvn-action", () => { } expect(error.message).toEqual( - expect.stringMatching(/^unexpected token*/i) + expect.stringMatching(/^.*is not valid json.*$/i) ); expect(postPeopleCanvassResponsesNock.isDone()).toEqual(false); diff --git a/__test__/extensions/action-handlers/zapier-action.test.js b/__test__/extensions/action-handlers/zapier-action.test.js index c9d7f3916..4eab9c2d8 100644 --- a/__test__/extensions/action-handlers/zapier-action.test.js +++ b/__test__/extensions/action-handlers/zapier-action.test.js @@ -10,6 +10,7 @@ const log = require("../../../src/lib").log; afterEach(async () => { jest.restoreAllMocks(); + await nock.cleanAll(); }); describe("zapier-action", () => { diff --git a/__test__/extensions/contact-loaders/csv-upload.test.js b/__test__/extensions/contact-loaders/csv-upload.test.js index 36cca7c32..a79f8dadb 100644 --- a/__test__/extensions/contact-loaders/csv-upload.test.js +++ b/__test__/extensions/contact-loaders/csv-upload.test.js @@ -87,7 +87,6 @@ describe("ingest-contact-loader method: csv-upload backend", () => { afterEach(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); it("csv-upload:available success/failure", async () => { diff --git a/__test__/extensions/contact-loaders/ngpvan/ngpvan.test.js b/__test__/extensions/contact-loaders/ngpvan/ngpvan.test.js index c108bf912..1dc40b3dc 100644 --- a/__test__/extensions/contact-loaders/ngpvan/ngpvan.test.js +++ b/__test__/extensions/contact-loaders/ngpvan/ngpvan.test.js @@ -1,5 +1,16 @@ +// client-testing libs +import React from "react"; +import { StyleSheetTestUtils } from "aphrodite"; +import { shallow } from "enzyme"; + import each from "jest-each"; import nock from "nock"; + +import Autocomplete from "@material-ui/lab/Autocomplete"; +import List from "@material-ui/core/List"; +import ListItem from "@material-ui/core/ListItem"; +import ListItemText from "@material-ui/core/ListItemText"; + import { getCellFromRow, getZipFromRow, @@ -9,10 +20,6 @@ import { getClientChoiceData, available } from "../../../../src/extensions/contact-loaders/ngpvan"; -import Autocomplete from "@material-ui/lab/Autocomplete"; -import List from "@material-ui/core/List"; -import ListItem from "@material-ui/core/ListItem"; -import ListItemText from "@material-ui/core/ListItemText"; import { CampaignContactsForm } from "../../../../src/extensions/contact-loaders/ngpvan/react-component"; @@ -22,10 +29,9 @@ const ngpvan = require("../../../../src/extensions/contact-loaders/ngpvan"); const helpers = require("../../../../src/extensions/contact-loaders/helpers"); const jobs = require("../../../../src/workers/jobs"); -// client-testing libs -import React from "react"; -import { shallow } from "enzyme"; -import { StyleSheetTestUtils } from "aphrodite"; +afterEach(async () => { + await nock.cleanAll(); +}); describe("ngpvan", () => { let fakeNgpVanBaseApiUrl; diff --git a/__test__/extensions/message-handlers/auto-optout.test.js b/__test__/extensions/message-handlers/auto-optout.test.js index ff9793bad..3c7d790c6 100644 --- a/__test__/extensions/message-handlers/auto-optout.test.js +++ b/__test__/extensions/message-handlers/auto-optout.test.js @@ -100,7 +100,6 @@ describe("Tests for Auto Opt-Out's members getting called from messageCache.save afterEach(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); beforeEach(async () => { diff --git a/__test__/extensions/message-handlers/profanity-tagger.test.js b/__test__/extensions/message-handlers/profanity-tagger.test.js index 70533bede..5fbacf948 100644 --- a/__test__/extensions/message-handlers/profanity-tagger.test.js +++ b/__test__/extensions/message-handlers/profanity-tagger.test.js @@ -20,7 +20,6 @@ beforeEach(async () => { afterEach(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); global.MESSAGE_HANDLERS = undefined; }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); @@ -63,10 +62,10 @@ describe("Message Hanlder: profanity-tagger", () => { // SEND await sendMessage(c.testContacts[1].id, c.testTexterUser, { - userId: c.testTexterUser.id, + userId: c.testTexterUser.id.toString(), contactNumber: c.testContacts[1].cell, text: "brass shoe eddie homonym", - assignmentId: c.assignmentId + assignmentId: c.assignmentId.toString() }); // a little stupidly updating messageservice_sid is necessary // because it's not await'd @@ -127,10 +126,10 @@ describe("Message Hanlder: profanity-tagger", () => { // Confirm texter catch await sendMessage(c.testContacts[0].id, c.testTexterUser, { - userId: c.testTexterUser.id, + userId: c.testTexterUser.id.toString(), contactNumber: c.testContacts[0].cell, text: "Some fakeslur message", - assignmentId: c.assignmentId + assignmentId: c.assignmentId.toString() }); const text1 = await r .knex("tag_campaign_contact") @@ -147,10 +146,10 @@ describe("Message Hanlder: profanity-tagger", () => { // Confirm texter no-match await sendMessage(c.testContacts[1].id, c.testTexterUser, { - userId: c.testTexterUser.id, + userId: c.testTexterUser.id.toString(), contactNumber: c.testContacts[1].cell, text: "brass shoe eddie homonym", - assignmentId: c.assignmentId + assignmentId: c.assignmentId.toString() }); const text2 = await r .knex("tag_campaign_contact") @@ -167,10 +166,10 @@ describe("Message Hanlder: profanity-tagger", () => { // Confirm texter no-match await sendMessage(c.testContacts[1].id, c.testTexterUser, { - userId: c.testTexterUser.id, + userId: c.testTexterUser.id.toString(), contactNumber: c.testContacts[1].cell, text: "fakeslur is one too many slurs", - assignmentId: c.assignmentId + assignmentId: c.assignmentId.toString() }); user = await cacheableData.user.userHasRole( c.testTexterUser, @@ -210,10 +209,10 @@ describe("Message Hanlder: profanity-tagger", () => { // Confirm texter catch await sendMessage(c.testContacts[0].id, c.testTexterUser, { - userId: c.testTexterUser.id, + userId: c.testTexterUser.id.toString(), contactNumber: c.testContacts[0].cell, text: "Some fakeslur message", - assignmentId: c.assignmentId + assignmentId: c.assignmentId.toString() }); const text1 = await r .knex("tag_campaign_contact") diff --git a/__test__/extensions/service-vendors/twilio.test.js b/__test__/extensions/service-vendors/twilio.test.js index cea2f4079..2c948a834 100644 --- a/__test__/extensions/service-vendors/twilio.test.js +++ b/__test__/extensions/service-vendors/twilio.test.js @@ -129,7 +129,6 @@ describe("twilio", () => { r.knex.removeListener("query", spokeDbListener); jest.restoreAllMocks(); await cleanupTest(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); it("should send messages", async () => { diff --git a/__test__/lib.test.js b/__test__/lib.test.js index 4800ee67a..2a80def30 100644 --- a/__test__/lib.test.js +++ b/__test__/lib.test.js @@ -2,7 +2,7 @@ import { resolvers } from "../src/server/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__/lib/timezones.test.js b/__test__/lib/timezones.test.js index 9342e9243..5f3086f76 100644 --- a/__test__/lib/timezones.test.js +++ b/__test__/lib/timezones.test.js @@ -14,7 +14,7 @@ import { getSendBeforeTimeUtc } from "../../src/lib/index"; -import { getProcessEnvDstReferenceTimezone } from "../../src/lib/tz-helpers"; +import * as tzHelpers from "../../src/lib/tz-helpers"; const makeCampignTextingHoursConfig = ( textingHoursEnforced, @@ -107,9 +107,6 @@ const buildIsBetweenTextingHoursExpectWithNoOffset = (start, end) => { ); }; -jest.unmock("../../src/lib/timezones"); -jest.mock("../../src/lib/tz-helpers"); - describe("test getLocalTime winter (standard time)", () => { beforeAll(() => { MockDate.set("2018-02-01T15:00:00Z"); @@ -120,7 +117,11 @@ describe("test getLocalTime winter (standard time)", () => { }); it("returns correct local time UTC-5 standard time", () => { - let localTime = getLocalTime(-5, true, getProcessEnvDstReferenceTimezone()); + let localTime = getLocalTime( + -5, + true, + tzHelpers.getProcessEnvDstReferenceTimezone() + ); expect(localTime.hours()).toEqual(10); expect(new Date(localTime)).toEqual( new Date("2018-02-01T10:00:00.000-05:00") @@ -138,7 +139,11 @@ describe("test getLocalTime summer (DST)", () => { }); it("returns correct local time UTC-5 DST", () => { - let localTime = getLocalTime(-5, true, getProcessEnvDstReferenceTimezone()); + let localTime = getLocalTime( + -5, + true, + tzHelpers.getProcessEnvDstReferenceTimezone() + ); expect(localTime.hours()).toEqual(11); expect(new Date(localTime)).toEqual( new Date("2018-07-21T10:00:00.000-05:00") @@ -147,9 +152,10 @@ describe("test getLocalTime summer (DST)", () => { }); describe("testing isBetweenTextingHours with env.TZ set", () => { - var tzHelpers = require("../../src/lib/tz-helpers"); beforeAll(() => { - tzHelpers.getProcessEnvTz.mockImplementation(() => "America/Los_Angeles"); + jest + .spyOn(tzHelpers, "getProcessEnvTz") + .mockImplementation(() => "America/Los_Angeles"); MockDate.set("2018-02-01T15:00:00.000-05:00"); }); @@ -360,10 +366,8 @@ describe("test isBetweenTextingHours with campaign overrides", () => { describe("test isBetweenTextingHours with offset data supplied", () => { var offsetData = { offset: -8, hasDST: true }; - var tzHelpers = require("../../src/lib/tz-helpers"); beforeAll(() => { - jest.doMock("../../src/lib/tz-helpers"); - tzHelpers.getProcessEnvTz.mockImplementation(() => null); + jest.spyOn(tzHelpers, "getProcessEnvTz").mockImplementation(() => null); MockDate.set("2018-02-01T12:00:00.000-08:00"); }); @@ -417,9 +421,8 @@ describe("test isBetweenTextingHours with offset data supplied", () => { describe("test isBetweenTextingHours with offset data empty", () => { var offsetData = { offset: null, hasDST: null }; - var tzHelpers = require("../../src/lib/tz-helpers"); beforeAll(() => { - tzHelpers.getProcessEnvTz.mockImplementation(() => null); + jest.spyOn(tzHelpers, "getProcessEnvTz").mockImplementation(() => null); }); afterEach(() => { @@ -466,9 +469,8 @@ describe("test isBetweenTextingHours with offset data empty", () => { }); describe("test isBetweenTextingHours with offset data NOT supplied", () => { - var tzHelpers = require("../../src/lib/tz-helpers"); beforeAll(() => { - tzHelpers.getProcessEnvTz.mockImplementation(() => null); + jest.spyOn(tzHelpers, "getProcessEnvTz").mockImplementation(() => null); }); afterEach(() => { @@ -515,10 +517,8 @@ describe("test isBetweenTextingHours with offset data NOT supplied", () => { }); describe("test defaultTimezoneIsBetweenTextingHours", () => { - var tzHelpers = require("../../src/lib/tz-helpers"); beforeAll(() => { - tzHelpers.getProcessEnvTz.mockImplementation(() => null); - jest.doMock("../../src/lib/tz-helpers"); + jest.spyOn(tzHelpers, "getProcessEnvTz").mockImplementation(() => null); }); afterEach(() => { @@ -646,8 +646,6 @@ describe("test getOffsets", () => { }); describe("test getContactTimezone", () => { - var tzHelpers = require("../../src/lib/tz-helpers"); - afterEach(() => { jest.resetAllMocks(); }); @@ -683,10 +681,8 @@ describe("test getContactTimezone", () => { describe("test isBetweenTextingHours with offset data supplied", () => { var offsetData = { offset: -8, hasDST: true }; - var tzHelpers = require("../../src/lib/tz-helpers"); beforeAll(() => { - jest.doMock("../../src/lib/tz-helpers"); - tzHelpers.getProcessEnvTz.mockImplementation(() => null); + jest.spyOn(tzHelpers, "getProcessEnvTz").mockImplementation(() => null); MockDate.set("2018-02-01T12:00:00.000-08:00"); }); @@ -740,9 +736,8 @@ describe("test isBetweenTextingHours with offset data supplied", () => { describe("test isBetweenTextingHours with offset data empty", () => { var offsetData = { offset: null, hasDST: null }; - var tzHelpers = require("../../src/lib/tz-helpers"); beforeAll(() => { - tzHelpers.getProcessEnvTz.mockImplementation(() => null); + jest.spyOn(tzHelpers, "getProcessEnvTz").mockImplementation(() => null); }); afterEach(() => { @@ -789,9 +784,8 @@ describe("test isBetweenTextingHours with offset data empty", () => { }); describe("test isBetweenTextingHours with offset data NOT supplied", () => { - var tzHelpers = require("../../src/lib/tz-helpers"); beforeAll(() => { - tzHelpers.getProcessEnvTz.mockImplementation(() => null); + jest.spyOn(tzHelpers, "getProcessEnvTz").mockImplementation(() => null); }); afterEach(() => { @@ -838,10 +832,8 @@ describe("test isBetweenTextingHours with offset data NOT supplied", () => { }); describe("test defaultTimezoneIsBetweenTextingHours", () => { - var tzHelpers = require("../../src/lib/tz-helpers"); beforeAll(() => { - tzHelpers.getProcessEnvTz.mockImplementation(() => null); - jest.doMock("../../src/lib/tz-helpers"); + jest.spyOn(tzHelpers, "getProcessEnvTz").mockImplementation(() => null); }); afterEach(() => { @@ -1004,8 +996,6 @@ describe("test getOffsets", () => { }); describe("test getContactTimezone", () => { - var tzHelpers = require("../../src/lib/tz-helpers"); - afterEach(() => { jest.resetAllMocks(); }); @@ -1039,7 +1029,9 @@ describe("test getContactTimezone", () => { }); it("uses TZ if no location is supplied, and the campaign doesn't override, and TZ exists in the environment", () => { - tzHelpers.getProcessEnvTz.mockImplementation(() => "America/Boise"); + jest + .spyOn(tzHelpers, "getProcessEnvTz") + .mockImplementation(() => "America/Boise"); expect( getContactTimezone( makeCampaignOnlyWithTextingHoursConfigFields( @@ -1163,8 +1155,6 @@ describe("test getUtcFromTimezoneAndHour", () => { }); describe("test getSendBeforeTimewUtc", () => { - const tzHelpers = require("../../src/lib/tz-helpers"); - beforeAll(() => { MockDate.set("2018-09-03T11:00:00.000-05:00"); }); @@ -1240,7 +1230,9 @@ describe("test getSendBeforeTimewUtc", () => { }); it("returns correct time if campaign does not override and TZ is set", () => { - tzHelpers.getProcessEnvTz.mockImplementation(() => "US/Eastern"); + jest + .spyOn(tzHelpers, "getProcessEnvTz") + .mockImplementation(() => "US/Eastern"); expect( getSendBeforeTimeUtc( {}, diff --git a/__test__/lib/tz-helpers.test.js b/__test__/lib/tz-helpers.test.js index 40c74e3e3..a710d2fbf 100644 --- a/__test__/lib/tz-helpers.test.js +++ b/__test__/lib/tz-helpers.test.js @@ -1,7 +1,5 @@ import { getProcessEnvDstReferenceTimezone } from "../../src/lib/tz-helpers"; -jest.unmock("../../src/lib/tz-helpers"); - describe("test getProcessEnvDstReferenceTimezone", () => { it("works", () => { expect(getProcessEnvDstReferenceTimezone()).toEqual("US/Eastern"); diff --git a/__test__/server/api/assignment.test.js b/__test__/server/api/assignment.test.js index 67508111c..3bba703c5 100644 --- a/__test__/server/api/assignment.test.js +++ b/__test__/server/api/assignment.test.js @@ -1,17 +1,16 @@ import { getContacts } from "../../../src/server/api/assignment"; import { Organization, Assignment, Campaign } from "../../../src/server/models"; -jest.mock("../../../src/lib/timezones.js"); -var timezones = require("../../../src/lib/timezones.js"); +import * as timezones from "../../../src/lib/timezones"; describe("test getContacts builds queries correctly", () => { - var organization = new Organization({ + const organization = new Organization({ texting_hours_enforced: false, texting_hours_start: 9, texting_hours_end: 14 }); - var campaign = new Campaign({ + const campaign = new Campaign({ due_by: new Date() }); @@ -19,12 +18,14 @@ describe("test getContacts builds queries correctly", () => { due_by: new Date().setFullYear(new Date().getFullYear() - 1) }); - var assignment = new Assignment({ + const assignment = new Assignment({ id: 1 }); beforeEach(() => { - timezones.getOffsets.mockReturnValueOnce([["-5_1"], ["-4_1"]]); + jest + .spyOn(timezones, "getOffsets") + .mockReturnValueOnce([["-5_1"], ["-4_1"]]); }); afterAll(() => { @@ -120,22 +121,24 @@ describe("test getContacts builds queries correctly", () => { }); // describe describe("test getContacts timezone stuff only", () => { - var organization = new Organization({ + const organization = new Organization({ texting_hours_enforced: true, texting_hours_start: 9, texting_hours_end: 14 }); - var campaign = new Campaign({ + const campaign = new Campaign({ due_by: new Date() }); - var assignment = new Assignment({ + const assignment = new Assignment({ id: 1 }); beforeEach(() => { - timezones.getOffsets.mockReturnValueOnce([["-5_1"], ["-4_1"]]); + jest + .spyOn(timezones, "getOffsets") + .mockReturnValueOnce([["-5_1"], ["-4_1"]]); }); afterAll(() => { @@ -143,8 +146,11 @@ describe("test getContacts timezone stuff only", () => { }); it("returns the correct query -- in default texting hours, with valid_timezone == true", () => { - timezones.defaultTimezoneIsBetweenTextingHours.mockReturnValueOnce(true); - var query = getContacts( + jest + .spyOn(timezones, "defaultTimezoneIsBetweenTextingHours") + .mockReturnValueOnce(true); + + const query = getContacts( assignment, { validTimezone: true }, organization, @@ -156,8 +162,11 @@ describe("test getContacts timezone stuff only", () => { }); // it it("returns the correct query -- in default texting hours, with valid_timezone == false", () => { - timezones.defaultTimezoneIsBetweenTextingHours.mockReturnValueOnce(true); - var query = getContacts( + jest + .spyOn(timezones, "defaultTimezoneIsBetweenTextingHours") + .mockReturnValueOnce(true); + + const query = getContacts( assignment, { validTimezone: false }, organization, @@ -169,8 +178,11 @@ describe("test getContacts timezone stuff only", () => { }); // it it("returns the correct query -- NOT in default texting hours, with valid_timezone == true", () => { - timezones.defaultTimezoneIsBetweenTextingHours.mockReturnValueOnce(false); - var query = getContacts( + jest + .spyOn(timezones, "defaultTimezoneIsBetweenTextingHours") + .mockReturnValueOnce(false); + + const query = getContacts( assignment, { validTimezone: true }, organization, @@ -182,8 +194,11 @@ describe("test getContacts timezone stuff only", () => { }); // it it("returns the correct query -- NOT in default texting hours, with valid_timezone == false", () => { - timezones.defaultTimezoneIsBetweenTextingHours.mockReturnValueOnce(false); - var query = getContacts( + jest + .spyOn(timezones, "defaultTimezoneIsBetweenTextingHours") + .mockReturnValueOnce(false); + + const query = getContacts( assignment, { validTimezone: false }, organization, @@ -195,21 +210,21 @@ describe("test getContacts timezone stuff only", () => { }); // it it("returns the correct query -- no contacts filter", () => { - var query = getContacts(assignment, null, organization, campaign); + const query = getContacts(assignment, null, organization, campaign); expect(query.toString()).toMatch( /^select \* from .campaign_contact. where .assignment_id. = 1.*/ ); }); // it it("returns the correct query -- no validTimezone property in contacts filter", () => { - var query = getContacts(assignment, {}, organization, campaign); + const query = getContacts(assignment, {}, organization, campaign); expect(query.toString()).toMatch( /^select \* from .campaign_contact. where .assignment_id. = 1.*/ ); }); // it it("returns the correct query -- validTimezone property is null", () => { - var query = getContacts( + const query = getContacts( assignment, { validTimezone: null }, organization, diff --git a/__test__/server/api/campaign/campaign.test.js b/__test__/server/api/campaign/campaign.test.js index 2589c5b19..ae9939dbf 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, @@ -64,7 +64,7 @@ beforeEach(async () => { testAdminUser = await createUser(); testInvite = await createInvite(); testOrganization = await createOrganization(testAdminUser, testInvite); - organizationId = testOrganization.data.createOrganization.id; + organizationId = testOrganization.data.createOrganization.id.toString(); testCampaign = await createCampaign(testAdminUser, testOrganization); testContacts = await createContacts(testCampaign, NUMBER_OF_CONTACTS); testTexterUser = await createTexter(testOrganization); @@ -82,7 +82,7 @@ beforeEach(async () => { ); await assignTexter(testAdminUser, testTexterUser, testCampaign); const dbCampaignContact = await getCampaignContact(testContacts[0].id); - assignmentId = dbCampaignContact.assignment_id; + assignmentId = dbCampaignContact.assignment_id.toString(); // await createScript(testAdminUser, testCampaign) // await startCampaign(testAdminUser, testCampaign) r.knex.on("query", spokeDbListener); @@ -93,7 +93,6 @@ afterEach(async () => { r.knex.removeListener("query", spokeDbListener); await cleanupTest(); jest.restoreAllMocks(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); it("allow supervolunteer to retrieve campaign data", async () => { @@ -177,7 +176,7 @@ it("save campaign interaction steps, edit it, make sure the last value is set", await createScript(testAdminUser, testCampaign); let campaignDataResults = await runGql( AdminCampaignEditQuery, - { campaignId: testCampaign.id }, + { campaignId: testCampaign.id.toString() }, testAdminUser ); expect(campaignDataResults.data.campaign.interactionSteps.length).toEqual(2); @@ -481,7 +480,7 @@ describe("Reassignments", () => { // send some texts for (let i = 0; i < 5; i++) { await sendMessage(testContacts[i].id, testTexterUser, { - userId: testTexterUser.id, + userId: testTexterUser.id.toString(), contactNumber: testContacts[i].cell, text: "test text", assignmentId @@ -512,11 +511,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) @@ -572,7 +571,7 @@ describe("Reassignments", () => { c => assignmentContacts2[i].id === c.id.toString() )[0]; const messageRes = await sendMessage(contact.id, testTexterUser2, { - userId: testTexterUser2.id, + userId: testTexterUser2.id.toString(), contactNumber: contact.cell, text: "test text autorespond", assignmentId: assignmentId2 @@ -630,7 +629,7 @@ describe("Reassignments", () => { ) )[0]; await sendMessage(contact.id, testTexterUser2, { - userId: testTexterUser2.id, + userId: testTexterUser2.id.toString(), contactNumber: contact.cell, text: "keep talking", assignmentId: assignmentId2 @@ -677,9 +676,17 @@ 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 + } ]); // TEXTER 1 (60 needsMessage, 5 messaged) // TEXTER 2 (25 needsMessage, 2 needsResponse, 3 convo) @@ -728,7 +735,7 @@ describe("Reassignments", () => { reassignCampaignContactsMutation, { organizationId, - newTexterUserId: testTexterUser2.id, + newTexterUserId: testTexterUser2.id.toString(), campaignIdsContactIds: [ { campaignId: testCampaign.id, @@ -782,23 +789,25 @@ describe("Reassignments", () => { expect(texterCampaignDataResults2.data.assignment.allContactsCount).toEqual( 31 ); + // bulkReassignCampaignContactsMutation await runGql( bulkReassignCampaignContactsMutation, { organizationId, - newTexterUserId: testTexterUser.id, + newTexterUserId: testTexterUser.id.toString(), contactsFilter: { messageStatus: "needsResponse", isOptedOut: false, validTimezone: true }, - campaignsFilter: { campaignId: testCampaign.id }, - assignmentsFilter: { texterId: testTexterUser2.id }, + campaignsFilter: { campaignId: parseInt(testCampaign.id) }, + assignmentsFilter: { texterId: parseInt(testTexterUser2.id) }, messageTextFilter: "" }, testAdminUser ); + // TEXTER 1 (60 needsMessage, 2 needsResponse, 4 messaged) // TEXTER 2 (25 needsMessage, 3 convo, 1 messaged) texterCampaignDataResults = await runGql( @@ -892,7 +901,10 @@ describe("Bulk Send", () => { ); // send some texts - const bulkSendResult = await bulkSendMessages(assignmentId, testTexterUser); + const bulkSendResult = await bulkSendMessages( + parseInt(assignmentId), + testTexterUser + ); resultTestFunction(bulkSendResult); // TEXTER 1 (95 needsMessage, 5 needsResponse) @@ -1046,7 +1058,7 @@ describe("campaigns query", () => { it("correctly filters by a single campaign id", async () => { const campaignsFilter = { - campaignId: testCampaign.id + campaignId: parseInt(testCampaign.id) }; const variables = { cursor, @@ -1061,7 +1073,7 @@ describe("campaigns query", () => { it("correctly filter by more than one campaign id", async () => { const campaignsFilter = { - campaignIds: [testCampaign.id, testCampaign2.id] + campaignIds: [parseInt(testCampaign.id), parseInt(testCampaign2.id)] }; const variables = { cursor, diff --git a/__test__/server/api/campaign/updateQuestionResponses.test.js b/__test__/server/api/campaign/updateQuestionResponses.test.js index 7073188a5..94037755f 100644 --- a/__test__/server/api/campaign/updateQuestionResponses.test.js +++ b/__test__/server/api/campaign/updateQuestionResponses.test.js @@ -58,7 +58,6 @@ describe("mutations.updateQuestionResponses", () => { afterEach(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); const saveInteractionStepsAndSendInitialMessages = async ( @@ -113,7 +112,7 @@ describe("mutations.updateQuestionResponses", () => { return sendMessage(contact.id, texterUser, { text: returnedInteractionSteps[0].script, contactNumber: contact.cell, - assignmentId: assignment.id, + assignmentId: assignment.id.toString(), userId: texterUser.id.toString() }); }); @@ -238,12 +237,12 @@ describe("mutations.updateQuestionResponses", () => { questionResponses = [ { - campaignContactId: contacts[0].id, + campaignContactId: contacts[0].id.toString(), interactionStepId: interactionSteps[0].id, value: colorInteractionSteps[0].answerOption }, { - campaignContactId: contacts[0].id, + campaignContactId: contacts[0].id.toString(), interactionStepId: redInteractionStep.id, value: shadesOfRedInteractionSteps[0].answerOption } @@ -276,7 +275,7 @@ describe("mutations.updateQuestionResponses", () => { `; const variables = { - ccid: contacts[0].id, + ccid: contacts[0].id.toString(), qr: questionResponses }; @@ -357,8 +356,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..ee0005b67 100644 --- a/__test__/server/api/createOptOut.test.js +++ b/__test__/server/api/createOptOut.test.js @@ -1,5 +1,5 @@ /* eslint-disable no-unused-expressions, consistent-return */ -import { r } from "../../../src/server/models/"; +import { r } from "../../../src/server/models"; import { setupTest, @@ -26,19 +26,18 @@ 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); afterEach(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); it("creates an opt out when the contact is assigned to the current user", async () => { diff --git a/__test__/server/api/editOrganization.test.js b/__test__/server/api/editOrganization.test.js index e244cebc6..02cda5bbc 100644 --- a/__test__/server/api/editOrganization.test.js +++ b/__test__/server/api/editOrganization.test.js @@ -3,7 +3,6 @@ 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, @@ -23,7 +22,6 @@ describe("editOrganization", () => { afterEach(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); describe("features", () => { diff --git a/__test__/server/api/errors.test.js b/__test__/server/api/errors.test.js index f2aee7d70..65065a877 100644 --- a/__test__/server/api/errors.test.js +++ b/__test__/server/api/errors.test.js @@ -23,7 +23,6 @@ describe("errors.js", () => { afterEach(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); describe("#authRequired", () => { @@ -40,10 +39,7 @@ 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."); }); }); diff --git a/__test__/server/api/joinOrganization.test.js b/__test__/server/api/joinOrganization.test.js index 685a296fd..6e39dee96 100644 --- a/__test__/server/api/joinOrganization.test.js +++ b/__test__/server/api/joinOrganization.test.js @@ -19,7 +19,6 @@ describe("joinOrganization", () => { afterEach(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); it("should add texters to a organization", async () => { diff --git a/__test__/server/api/mutations/updateServiceVendorConfig.test.js b/__test__/server/api/mutations/updateServiceVendorConfig.test.js index 015794b3c..ba2c4aa9d 100644 --- a/__test__/server/api/mutations/updateServiceVendorConfig.test.js +++ b/__test__/server/api/mutations/updateServiceVendorConfig.test.js @@ -10,6 +10,7 @@ import { createOrganization, createUser, ensureOrganizationTwilioWithMessagingService, + flushRedis, runGql, setupTest } from "../../../test_helpers"; @@ -20,7 +21,6 @@ describe("updateServiceVendorConfig", () => { }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); afterEach(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); jest.restoreAllMocks(); }); @@ -85,7 +85,7 @@ describe("updateServiceVendorConfig", () => { const dbOrganization = await Organization.get(organization.id); dbOrganization.features = null; await dbOrganization.save(); - if (r.redis) r.redis.flushdb(); + await flushRedis(); }); it("returns an error", async () => { const gqlResult = await runGql(updateServiceVendorConfigGql, vars, user); @@ -161,7 +161,7 @@ describe("updateServiceVendorConfig", () => { dbOrganization = await Organization.get(organization.id); dbOrganization.features = JSON.stringify({ service: "twilio" }); await dbOrganization.save(); - if (r.redis) r.redis.flushdb(); + await flushRedis(); expectedFeatures = { service, @@ -219,7 +219,7 @@ describe("updateServiceVendorConfig", () => { [configKey]: "it doesn't matter" }); await dbOrganization.save(); - if (r.redis) r.redis.flushdb(); + await flushRedis(); }); it("writes message service config in features.configKey", async () => { const gqlResult = await runGql( @@ -249,7 +249,7 @@ describe("updateServiceVendorConfig", () => { TWILIO_MESSAGE_SERVICE_SID: "the_former_fake_message_service_sid" }); await dbOrganization.save(); - if (r.redis) r.redis.flushdb(); + await flushRedis(); }); it("writes individual config components to the top level of features", async () => { const gqlResult = await runGql( @@ -281,7 +281,7 @@ describe("updateServiceVendorConfig", () => { TWILIO_MESSAGE_SERVICE_SID: "the_former_fake_message_service_sid" }); await dbOrganization.save(); - if (r.redis) r.redis.flushdb(); + await flushRedis(); extremelyFakeService = { updateConfig: jest.fn().mockImplementation(() => { diff --git a/__test__/server/api/organization.test.js b/__test__/server/api/organization.test.js index fbdfd4083..7ce302ec5 100644 --- a/__test__/server/api/organization.test.js +++ b/__test__/server/api/organization.test.js @@ -1,8 +1,8 @@ /* eslint-disable no-unused-expressions, consistent-return */ import { isSqlite, r } from "../../../src/server/models/"; import { getCampaignsQuery } from "../../../src/containers/AdminCampaignList"; -import { GraphQLError } from "graphql/error"; -import gql from "graphql-tag"; +import { GraphQLError } from "graphql"; +import { gql } from "@apollo/client"; import * as messagingServices from "../../../src/extensions/service-vendors"; import { @@ -49,7 +49,6 @@ describe("organization", () => { afterEach(async () => { await cleanupTest(); jest.restoreAllMocks(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); describe("organization query", () => { @@ -92,7 +91,7 @@ describe("organization", () => { it("filters by a single campaign id", async () => { variables.campaignsFilter = { - campaignId: testCampaign.id + campaignId: parseInt(testCampaign.id) }; const result = await runGql(getCampaignsQuery, variables, testAdminUser); @@ -104,7 +103,7 @@ describe("organization", () => { it("filter by more than one campaign id", async () => { const campaignsFilter = { - campaignIds: [testCampaign.id, testCampaign2.id] + campaignIds: [parseInt(testCampaign.id), parseInt(testCampaign2.id)] }; variables.campaignsFilter = campaignsFilter; @@ -235,7 +234,7 @@ describe("organization", () => { organizationQuery = ` query q($organizationId: String!) { organization(id: $organizationId) { - id + id name availableActions { name @@ -314,7 +313,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..ed4085604 100644 --- a/__test__/server/api/people.test.js +++ b/__test__/server/api/people.test.js @@ -1,12 +1,12 @@ /** - * @jest-environment jsdom + * @jest-environment node */ /* eslint-disable no-unused-expressions, consistent-return */ import { r } from "../../../src/server/models/"; import { getUsersGql } from "../../../src/containers/PeopleList"; -import { GraphQLError } from "graphql/error"; +import { GraphQLError } from "graphql"; import { resolvers } from "../../../src/server/api/schema"; -import { validate as uuidValidate } from 'uuid'; +import { validate as uuidValidate } from "uuid"; import { setupTest, @@ -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 @@ -191,7 +191,6 @@ describe("people", () => { afterEach(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); describe("filtering", () => { @@ -208,16 +207,20 @@ describe("people", () => { it("filters users to those assigned to a single campaign", async () => { const campaignsFilter = { - campaignId: testCampaigns[0].id + campaignId: parseInt(testCampaigns[0].id) }; variables.campaignsFilter = campaignsFilter; const result = await runGql(getUsersGql, variables, testAdminUsers[0]); + await testFiltering(result, [testTexterUsers[0], testTexterUsers[1]]); }); it("filters users to those assigned to multiple campaigns", async () => { const campaignsFilter = { - campaignIds: [testCampaigns[0].id, testCampaigns[3].id] + campaignIds: [ + parseInt(testCampaigns[0].id), + parseInt(testCampaigns[3].id) + ] }; 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 }, - 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(); }); @@ -518,11 +529,17 @@ describe("people", () => { it("reset Auth0 password", () => { // Remove PASSPORT_STRATEGY env var. PASSPORT_STRATEGY will default to "auth0" if there's nothing explicitly set - delete window.PASSPORT_STRATEGY; + delete process.env.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..3c96c5a74 100644 --- a/__test__/server/api/updateContactTags.test.js +++ b/__test__/server/api/updateContactTags.test.js @@ -32,7 +32,6 @@ describe("mutations.updateContactTags", () => { afterEach(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); beforeEach(async () => { @@ -106,7 +105,7 @@ describe("mutations.updateContactTags", () => { const result = await wrappedMutations.updateContactTags( contactTags, - contacts[0].id + contacts[0].id.toString() ); expect(result.data.updateContactTags).toEqual({ @@ -154,12 +153,11 @@ 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( expect.stringMatching( - /^The loader.load\(\) function must be called with a value,but got: undefined.*/ + /^The loader.load\(\) function must be called with a value, but got: undefined.*/ ) ); diff --git a/__test__/server/lib/http-request.test.js b/__test__/server/lib/http-request.test.js index f1a4694c8..14bfc8d52 100644 --- a/__test__/server/lib/http-request.test.js +++ b/__test__/server/lib/http-request.test.js @@ -3,6 +3,10 @@ import nock from "nock"; import requestWithRetry from "../../../src/server/lib/http-request.js"; +afterEach(async () => { + await nock.cleanAll(); +}); + describe("requestWithRetry", () => { let url; let path; diff --git a/__test__/server/models/cacheable_queries/campaign-contact.test.js b/__test__/server/models/cacheable_queries/campaign-contact.test.js index 270da04f0..ece444ce5 100644 --- a/__test__/server/models/cacheable_queries/campaign-contact.test.js +++ b/__test__/server/models/cacheable_queries/campaign-contact.test.js @@ -22,7 +22,6 @@ describe("CampaignContactCache", () => { afterEach(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); beforeEach(async () => { diff --git a/__test__/server/models/cacheable_queries/tag-campaign-contact.test.js b/__test__/server/models/cacheable_queries/tag-campaign-contact.test.js index a60805feb..76e6b20f4 100644 --- a/__test__/server/models/cacheable_queries/tag-campaign-contact.test.js +++ b/__test__/server/models/cacheable_queries/tag-campaign-contact.test.js @@ -71,9 +71,6 @@ describe("cacheable_queries.tagCampaignContactCache", () => { afterEach(async () => { jest.restoreAllMocks(); await cleanupTest(); - if (r.redis) { - r.redis.flushdb(); - } }); describe(".save", () => { diff --git a/__test__/server/models/cacheable_queries/user.test.js b/__test__/server/models/cacheable_queries/user.test.js index 664327324..3e8ba615c 100644 --- a/__test__/server/models/cacheable_queries/user.test.js +++ b/__test__/server/models/cacheable_queries/user.test.js @@ -36,9 +36,6 @@ describe("cacheable_queries.user", () => { }); organizationId = testOrganization.data.createOrganization.id; - if (r.redis) { - await r.redis.flushdb(); - } queryLog = []; r.knex.on("query", spokeDbListener); }); diff --git a/__test__/server/models/question-response.test.js b/__test__/server/models/question-response.test.js index 69fbc256c..911518060 100644 --- a/__test__/server/models/question-response.test.js +++ b/__test__/server/models/question-response.test.js @@ -16,7 +16,6 @@ describe("questionResponse cacheableData methods", () => { afterEach(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); it("save and load", async () => { @@ -25,7 +24,7 @@ describe("questionResponse cacheableData methods", () => { steps: 3, choices: 2 }); - const interactionSteps = await r.knex("interaction_step").select(); + await r.knex("interaction_step").select(); let saveResult = await cacheableData.questionResponse.save(cid, [ { interactionStepId: "1", value: "hmm1" } @@ -41,7 +40,7 @@ describe("questionResponse cacheableData methods", () => { expect(questionResponses.length).toBe(1); expect(questionResponses[0].value).toBe("hmm1"); expect(questionResponses[0].interaction_step_id).toBe(1); - let firstCreatedAt = questionResponses[0].created_at; + const firstCreatedAt = questionResponses[0].created_at; // cached questionResponses = await cacheableData.questionResponse.query(cid, true); expect(questionResponses.length).toBe(1); diff --git a/__test__/server/models/tags.test.js b/__test__/server/models/tags.test.js index 851db1584..82d229534 100644 --- a/__test__/server/models/tags.test.js +++ b/__test__/server/models/tags.test.js @@ -48,7 +48,6 @@ describe("A tag model", () => { afterEach(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); it("holds all tags with name, description, andr group, and tags belong to organizations", async () => { @@ -120,7 +119,6 @@ describe("A tag model", () => { afterEach(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); it("is applied to a contact, and associated to a campaign, and can hold a value that explains why that tag was applied to that message", async () => { diff --git a/__test__/server/texter.test/common.js b/__test__/server/texter.test/common.js index 8b5fd6f86..b32c85221 100644 --- a/__test__/server/texter.test/common.js +++ b/__test__/server/texter.test/common.js @@ -46,7 +46,6 @@ beforeEach(async () => { afterEach(async () => { await cleanupTest(); - if (r.redis) r.redis.flushdb(); }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); export { diff --git a/__test__/server/texter.test/correctContactsAfterReassignment.test.js b/__test__/server/texter.test/correctContactsAfterReassignment.test.js index face6557b..db50b8d93 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 ); @@ -81,7 +81,7 @@ it("should return contacts after they are reassigned", async () => { assignmentId: newAssignmentId } }, - 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..9f232b3f1 100644 --- a/__test__/server/texter.test/texter.test.js +++ b/__test__/server/texter.test/texter.test.js @@ -42,7 +42,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: {} } }; @@ -63,9 +63,9 @@ it("should send an initial message to test contacts", async () => { const message = { contactNumber: contact.cell, - userId: testTexterUser.id, + userId: testTexterUser.id.toString(), text: "test text", - assignmentId + assignmentId: assignmentId.toString() }; const [messageMutation, messageVars] = sendMessageMutAndVars( @@ -115,7 +115,7 @@ 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: {} } }; @@ -136,9 +136,9 @@ it("should be able to receive a response and reply (using fakeService)", async ( const message = { contactNumber: contact.cell, - userId: testTexterUser.id, + userId: testTexterUser.id.toString(), text: "test text autorespond", - assignmentId + assignmentId: assignmentId.toString() }; const [messageMutation, messageVars] = sendMessageMutAndVars( @@ -178,9 +178,9 @@ it("should be able to receive a response and reply (using fakeService)", async ( // 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( diff --git a/__test__/setup.js b/__test__/setup.js index 43c222a61..995735977 100644 --- a/__test__/setup.js +++ b/__test__/setup.js @@ -1,7 +1,13 @@ import { configure } from "enzyme"; import Adapter from "enzyme-adapter-react-16"; +import { flushRedis } from "./test_helpers"; + configure({ adapter: new Adapter() }); // server/api/campaign.test.js has some long tests so we increase from 5sec default jest.setTimeout(15000); + +beforeEach(async () => { + await flushRedis(); +}); diff --git a/__test__/test_helpers.js b/__test__/test_helpers.js index 377a92a8b..f111833ec 100644 --- a/__test__/test_helpers.js +++ b/__test__/test_helpers.js @@ -9,7 +9,9 @@ import { r } from "../src/server/models/"; import { graphql } from "graphql"; -import gql from "graphql-tag"; +import { makeExecutableSchema } from "@graphql-tools/schema"; +import { resolvers } from "../src/server/api/schema"; +import { schema as apiSchema } from "../src/api/schema"; // 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. @@ -22,8 +24,19 @@ export async function setupTest() { await createTables(); } -export async function cleanupTest() { +export async function flushRedis() { + if (r.redis) { + for await (const key of r.redis.scanIterator({ MATCH: "*", COUNT: 1 })) { + r.redis.FLUSHDB(); + } + } +} + +export async function cleanupTest(doFlushRedis = true) { await dropTables(); + if (doFlushRedis) { + await flushRedis(); + } } export function sleep(ms) { @@ -83,12 +96,8 @@ export async function createContacts(campaign, count = 1) { 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 }); @@ -100,17 +109,17 @@ function getGqlOperationText(op) { return op.loc && op.loc.source.body; } -export async function runGql(operation, vars, user) { +export async function runGql(operation, variableValues, user) { const operationText = 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: operationText, rootValue, - context, - vars - ); + contextValue, + variableValues + }); if (result && result.errors) { console.log("runGql failed " + JSON.stringify(result)); } @@ -134,7 +143,7 @@ export const updateUserRoles = async ( const variables = { organizationId, - userId, + userId: userId.toString(), roles }; const result = await runGql(query, variables, adminUser); @@ -151,17 +160,23 @@ export async function createInvite() { id } }`; - const context = getContext(); - return await graphql(mySchema, inviteQuery, rootValue, context); + const contextValue = getContext(); + return await graphql({ + schema, + source: inviteQuery, + rootValue, + contextValue + }); } export async function createOrganization(user, invite) { const rootValue = {}; const name = "Testy test organization"; - const userId = user.id; + const userId = user.id.toString(); + 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) { @@ -170,18 +185,18 @@ export async function createOrganization(user, invite) { } }`; - const variables = { + const variableValues = { userId, name, inviteId }; - const result = await graphql( - mySchema, - orgQuery, + const result = await graphql({ + schema, + source: orgQuery, rootValue, - context, - variables - ); + contextValue, + variableValues + }); if (result && result.errors) { throw new Error("createOrganization failed " + JSON.stringify(result)); } @@ -238,7 +253,7 @@ 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 = ` mutation updateServiceVendorConfig( @@ -263,13 +278,19 @@ 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: query, + rootValue, + contextValue, + variableValues + }); if (result && result.errors) { console.log("updateServiceVendorConfig failed " + JSON.stringify(result)); } @@ -285,28 +306,28 @@ 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 variables = { + const variableValues = { input: { title, description, - organizationId, + organizationId: organizationId.toString(), ...args } }; - const result = await graphql( - mySchema, - campaignQuery, + const result = await graphql({ + schema, + source: campaignQuery, rootValue, - context, - variables - ); + contextValue, + variableValues + }); if (result.errors) { throw new Error("Create campaign failed " + JSON.stringify(result)); } @@ -323,8 +344,8 @@ export async function saveCampaign( const rootValue = {}; const description = "test description"; const organizationId = campaign.organizationId; - const campaignId = campaign.id; - const context = getContext({ user }); + const campaignId = campaign.id.toString(); + const contextValue = getContext({ user }); const campaignQuery = `mutation editCampaign($campaignId: String!, $campaign: CampaignInput!) { editCampaign(id: $campaignId, campaign: $campaign) { @@ -334,7 +355,7 @@ export async function saveCampaign( } }`; - const variables = { + const variableValues = { campaign: { title, description, @@ -342,13 +363,13 @@ export async function saveCampaign( }, campaignId }; - const result = await graphql( - mySchema, - campaignQuery, + const result = await graphql({ + schema, + source: campaignQuery, rootValue, - context, - variables - ); + contextValue, + variableValues + }); if (result.errors) { throw new Error("Create campaign failed " + JSON.stringify(result)); } @@ -371,12 +392,12 @@ export async function saveCampaign( fullyConfigured } }`; - const managerResult = await graphql( - mySchema, - serviceManagerQuery, + const managerResult = await graphql({ + schema, + source: serviceManagerQuery, rootValue, - context, - { + contextValue, + variableValues: { organizationId, serviceManagerName: "per-campaign-messageservices", updateData: { @@ -385,7 +406,7 @@ export async function saveCampaign( }, campaignId } - ); + }); console.log("managerResult", JSON.stringify(managerResult)); } @@ -399,8 +420,14 @@ export async function copyCampaign(campaignId, user) { id } }`; - const context = getContext({ user }); - return await graphql(mySchema, query, rootValue, context, { campaignId }); + const contextValue = getContext({ user }); + return await graphql({ + schema, + source: query, + rootValue, + contextValue, + variableValues: { campaignId } + }); } export async function createTexter(organization, userInfo = {}) { @@ -426,17 +453,17 @@ export async function createTexter(organization, userInfo = {}) { 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: joinQuery, rootValue, - context, - variables - ); + contextValue, + variableValues + }); if (result.errors) { throw new Error("joinOrganization failed " + JSON.stringify(result)); } @@ -452,34 +479,35 @@ export async function assignTexter(admin, user, campaign, assignments) { const rootValue = {}; const campaignEditQuery = ` mutation editCampaign($campaignId: String!, $campaign: CampaignInput!) { - editCampaign(id: $campaignId, campaign: $campaign) { + editCampaign( + id: $campaignId, + campaign: $campaign) { id - assignments { - id - } + assignments { id } } }`; - const context = getContext({ user: admin }); - const updateCampaign = Object.assign({}, campaign); + const contextValue = getContext({ user: admin }); + const updateCampaign = { ...campaign }; const campaignId = updateCampaign.id; updateCampaign.texters = assignments || [ { - id: user.id + id: user.id.toString() } ]; delete updateCampaign.id; delete updateCampaign.contacts; - const variables = { - campaignId, + const variableValues = { + campaignId: campaignId.toString(), campaign: updateCampaign }; - const result = await graphql( - mySchema, - campaignEditQuery, + + const result = await graphql({ + schema, + source: campaignEditQuery, rootValue, - context, - variables - ); + contextValue, + variableValues + }); if (result.errors) { throw new Error("assignTexter failed " + JSON.stringify(result)); } @@ -501,12 +529,19 @@ export async function sendMessage(campaignContactId, user, message) { } } }`; - const context = getContext({ user }); - const variables = { + const contextValue = getContext({ user }); + const variableValues = { message, - campaignContactId + campaignContactId: campaignContactId.toString() }; - const result = await graphql(mySchema, query, rootValue, context, variables); + + const result = await graphql({ + schema, + source: query, + rootValue, + contextValue, + variableValues + }); if (result.errors) { console.log("sendMessage errors", result); } @@ -574,21 +609,21 @@ export async function createScript( builtInteractionSteps = buildScript(steps, choices); } - const context = getContext({ user: admin }); + const contextValue = getContext({ user: admin }); const campaignId = campaign.id; - const variables = { + const variableValues = { campaignId, campaign: { interactionSteps: interactionSteps || builtInteractionSteps[0] } }; - return await graphql( - mySchema, - campaignEditQuery, + return await graphql({ + schema, + source: campaignEditQuery, rootValue, - context, - variables - ); + contextValue, + variableValues + }); } export async function createCannedResponses(admin, campaign, cannedResponses) { @@ -600,21 +635,21 @@ export async function createCannedResponses(admin, campaign, cannedResponses) { 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: campaignEditQuery, rootValue, - context, - variables - ); + contextValue, + variableValues + }); } export async function startCampaign(admin, campaign) { @@ -624,15 +659,15 @@ export async function startCampaign(admin, campaign) { 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: startCampaignQuery, rootValue, - context, - variables - ); + contextValue, + variableValues + }); } export async function getCampaignContact(id) { @@ -777,7 +812,7 @@ export const getConversations = async ( export const createJob = async (campaign, overrides) => { const job = { - campaign_id: campaign.id, + campaign_id: campaign.id.toString(), payload: "fake_payload", queue_name: "1:fake_queue_name", job_type: "fake_job_type", diff --git a/__test__/workers/jobs.test.js b/__test__/workers/jobs.test.js index e9179e2bc..9da5833c4 100644 --- a/__test__/workers/jobs.test.js +++ b/__test__/workers/jobs.test.js @@ -11,42 +11,41 @@ import { createOrganization } from "../test_helpers"; -jest.mock("../../src/lib/zip-format"); -var zipFormat = require("../../src/lib/zip-format"); +import * as zipFormat from "../../src/lib/zip-format"; describe("test getTimezoneByZip", () => { beforeAll( async () => await setupTest(), global.DATABASE_SETUP_TEARDOWN_TIMEOUT ); - afterAll( - async () => await cleanupTest(), - global.DATABASE_SETUP_TEARDOWN_TIMEOUT - ); + afterAll(async () => { + jest.resetAllMocks(); + await cleanupTest(); + }, global.DATABASE_SETUP_TEARDOWN_TIMEOUT); it("returns timezone data from the common zipcode/timezone mappings", async () => { - zipFormat.zipToTimeZone.mockReturnValueOnce([0, 0, 3, 1]); + jest.spyOn(zipFormat, "zipToTimeZone").mockReturnValueOnce([0, 0, 3, 1]); var good_things_come_to_those_who_wait = await getTimezoneByZip("11790"); expect(good_things_come_to_those_who_wait).toEqual("3_1"); }); it("does not memoize common zipcode/timezone mappings", async () => { - zipFormat.zipToTimeZone.mockReturnValueOnce([0, 0, 4, 1]); + jest.spyOn(zipFormat, "zipToTimeZone").mockReturnValueOnce([0, 0, 4, 1]); var future = await getTimezoneByZip("11790"); expect(future).toEqual("4_1"); }); it("does not find a zipcode in the database!", async () => { - zipFormat.zipToTimeZone.mockReturnValueOnce(undefined); + jest.spyOn(zipFormat, "zipToTimeZone").mockReturnValueOnce(undefined); var future = await getTimezoneByZip("11790"); expect(future).toEqual(""); }); it("finds a zipcode in the database and memoizes it", async () => { - zipFormat.zipToTimeZone.mockReturnValueOnce(undefined); + jest.spyOn(zipFormat, "zipToTimeZone").mockReturnValueOnce(undefined); try { var zipCode = new ZipCode({ diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 000000000..f2f34b7d0 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,25 @@ +// The .babelrc config file takes priority for app compilation. +// For running Jest tests, this file takes precedence over the settings in .babelrc + +module.exports = { + presets: [ + "@babel/preset-react", + "@babel/preset-env", + ["@babel/preset-typescript", { allExtensions: true, isTSX: true }] + ], + only: ["./**/*.js", "./**/*.jsx"], + plugins: [ + "@babel/plugin-proposal-export-default-from", + [ + "@babel/plugin-transform-runtime", + { + regenerator: true + } + ] + ], + env: { + dev: { + plugins: ["react-hot-loader/babel"] + } + } +}; diff --git a/dev-tools/.env.test b/dev-tools/.env.test index 341b8b3d4..3c5c11994 100644 --- a/dev-tools/.env.test +++ b/dev-tools/.env.test @@ -7,6 +7,7 @@ OUTPUT_DIR=./build ASSETS_DIR=./build/client/assets ASSETS_MAP_FILE=assets.json DB_HOST=127.0.0.1 +DB_PORT=5432 DB_NAME=spoke_test DB_USER=spoke_test DB_PASSWORD=spoke_test diff --git a/dev-tools/create-test-database b/dev-tools/create-test-database index 693dd00db..1aa4f61eb 100755 --- a/dev-tools/create-test-database +++ b/dev-tools/create-test-database @@ -1,7 +1,7 @@ #!/bin/bash set -euo pipefail -docker-compose exec -T postgres psql -h localhost -p 5432 -U spoke spokedev < Owner` - - Make sure the `JSON` radio button is selected (it should be selected by default) - - Click `Create` - - Wait. -13. Another file will be downloaded. The name of the file (which will have the extension `json`) will be displayed when the download is complete. -14. Open the file. -15. The contents will look like this: + - You may skip the opition "Grand Users access to this service account" +6. Return to the `Google Docs API` page, and on the left, click `Credentials` and then click `ADD KEY`. + - Set type to JSON and continue. +8. Another file will be downloaded. The name of the file (which will have the extension `json`) will be displayed when the download is complete. +9. Open the file. +10. The contents will look like this: ``` { "type": "service_account", "project_id": "quickstart-1552345943126", "private_key_id": "1f029699545c3a00039b7ed0894f60d8bccfb970", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDLfJOh++1qhThG\nYE3IPpgdaNFGjmOU+xlBJJY6Ff1XIFmCD8qm2eVaByHVWk9o8ZBxFSYx/fgAiMYK\nJ8lun9hKZA7o7ecHQJt89j8QjLXlpfwuo11KZ+UAA46VqCtmnFuEtpZ5A3sVjc1D\nx9Xdf2VJPNA472nInBIvfi5QaawplICx8f2IbotGPxpNraaJu82vN7VO4n2qABor\nXL0+5d5av6jSbnpf3KJUtDlwxWBLZWmQ8G7WUXFUGYOD4fDMF+xhSrL0VVE+cavI\n28rEfShctWv1c7TZazEZwpK0Zq6e6CzstwBk3rGd950lxcfqueNSg/Ejlp5mSYWH\n8ms+FwLnAgMBAAECggEASFA7PuOCmcpCF3B9892avUjUplhPt1AMx4OzB21tHJtY\nc8oc4HKq+PVz5pgzhD3kcOttKXLxwW7Zwh4ljXSsrrMkQU7aPU+OcjgobT80HSqB\nlilkK98EGJ8q+rBKzCpgs6cXjmXYRe6gtae8rvxpCD/eV31tgGdGZy5WUylaj2Oe\notMtjNm7TbpYiYg/SIuV1o6luJwnxi9wZBrYsrEhI6rWgJ9Q5ULmNijXyApZJ7Gy\nNLLJSVr5J9RtoY+/Uz2Mbm74fYcVHDU4Yy5jkxwROkV1i3onaUdmwHbe58VgcWRN\ntBmAbuPYiFe8uXNqtBDzMLx+Fo+tYBFnmq66vTX0YQKBgQDn3wAFMFC55kQW0MOn\nbpzBGTAUQLeIxBlTfuYniqTAigPI1tlljph6dTlfL/VfzIix1eb0h/fHHWyZ02WU\nIZ7PzefMoTnm9v91VBsjmynDZ/OQKjeU/IBYd72uKOxI8lauZs1Wyp+bLWjCz7vV\n9BFG9BzKQ5eB79CVv1GixYxfLwKBgQDgqWuJJoHXiUqt+QZ6eDwQO9juxpy9bfH1\nnSYjPXp5R90cxgg3FWr8F4E03Da38u06gdi31jqCJoWiSVhdbKrXopVodmrmsUtA\nR0KAleg4XpRENMKF35zIJYeXug715o2XUPZ5H8tT8q0tuqgYFsI1PyroUd/lIfVj\n0AhrqAppyQKBgQCt7dEOE1f6moeotaCOD6L2FfbCumjx5mc5Ao+SSaWb5+s+1Cru\nyzAFa7lFdawR2FMRUuqTswpiCehU2wXvP+jo6ANgs+/DGLQ3Roe1BccmFOvW0FQx\nJdcAhZF6+qeDcIUk/Wg6GnPu6vkSaND1hMcQ+jw+XMVhaoqESabq+lR5cQKBgQC2\nqWkykOl++jSK8O9QghOry00dDsT/y7Wv0n7gpiq/Eyv3Khgh2TssDlxSQz4GH/C7\n4jj3d6oIihObGHFNPH5HZvx9e9J9EOezMn0imT+/HT8FmbQTLvWFUeZF+dQSIMs8\nnWpYnv4tmiEuDhZ/x3lN27ciPveAkDS5W7qM9YrJ6QKBgQDHenfweOPuZxFmfqqG\nao/GuNLdCn8c2ZJzUJR5NNpaXnUlDxEgOmaeAw9quRNtdVAVHBnPC4uFegc7VBcA\n9guK7uF7Q1rB1Ypw8qzuEBz6BDQ9zc4q026xrPXADVK6zqC/CvRhbYaTgykZaCJl\nozRrvrZDHjjgC5wC2+d278NcFQ==\n-----END PRIVATE KEY-----\n", + "private_key": "-----BEGIN PRIVATE KEY-----\naVeryLongPrivateKey\n-----END PRIVATE KEY-----\n", "client_email": "test-252@quickstart-1552345943126.iam.gserviceaccount.com", "client_id": "103778937997709997381", "auth_uri": "https://accounts.google.com/o/oauth2/auth", @@ -61,7 +45,7 @@ It's a horrible idea to publish live secrets to Github. You should never do that 16. Delete all the new lines so the entire contents of the file appears on a single line, and that line should not have a hard line end. It should be the only line in the file. It should look like this: ``` -{ "type": "service_account", "project_id": "quickstart-1552345943126", "private_key_id": "1f029699545c3a00039b7ed0894f60d8bccfb970", "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDLfJOh++1qhThG\nYE3IPpgdaNFGjmOU+xlBJJY6Ff1XIFmCD8qm2eVaByHVWk9o8ZBxFSYx/fgAiMYK\nJ8lun9hKZA7o7ecHQJt89j8QjLXlpfwuo11KZ+UAA46VqCtmnFuEtpZ5A3sVjc1D\nx9Xdf2VJPNA472nInBIvfi5QaawplICx8f2IbotGPxpNraaJu82vN7VO4n2qABor\nXL0+5d5av6jSbnpf3KJUtDlwxWBLZWmQ8G7WUXFUGYOD4fDMF+xhSrL0VVE+cavI\n28rEfShctWv1c7TZazEZwpK0Zq6e6CzstwBk3rGd950lxcfqueNSg/Ejlp5mSYWH\n8ms+FwLnAgMBAAECggEASFA7PuOCmcpCF3B9892avUjUplhPt1AMx4OzB21tHJtY\nc8oc4HKq+PVz5pgzhD3kcOttKXLxwW7Zwh4ljXSsrrMkQU7aPU+OcjgobT80HSqB\nlilkK98EGJ8q+rBKzCpgs6cXjmXYRe6gtae8rvxpCD/eV31tgGdGZy5WUylaj2Oe\notMtjNm7TbpYiYg/SIuV1o6luJwnxi9wZBrYsrEhI6rWgJ9Q5ULmNijXyApZJ7Gy\nNLLJSVr5J9RtoY+/Uz2Mbm74fYcVHDU4Yy5jkxwROkV1i3onaUdmwHbe58VgcWRN\ntBmAbuPYiFe8uXNqtBDzMLx+Fo+tYBFnmq66vTX0YQKBgQDn3wAFMFC55kQW0MOn\nbpzBGTAUQLeIxBlTfuYniqTAigPI1tlljph6dTlfL/VfzIix1eb0h/fHHWyZ02WU\nIZ7PzefMoTnm9v91VBsjmynDZ/OQKjeU/IBYd72uKOxI8lauZs1Wyp+bLWjCz7vV\n9BFG9BzKQ5eB79CVv1GixYxfLwKBgQDgqWuJJoHXiUqt+QZ6eDwQO9juxpy9bfH1\nnSYjPXp5R90cxgg3FWr8F4E03Da38u06gdi31jqCJoWiSVhdbKrXopVodmrmsUtA\nR0KAleg4XpRENMKF35zIJYeXug715o2XUPZ5H8tT8q0tuqgYFsI1PyroUd/lIfVj\n0AhrqAppyQKBgQCt7dEOE1f6moeotaCOD6L2FfbCumjx5mc5Ao+SSaWb5+s+1Cru\nyzAFa7lFdawR2FMRUuqTswpiCehU2wXvP+jo6ANgs+/DGLQ3Roe1BccmFOvW0FQx\nJdcAhZF6+qeDcIUk/Wg6GnPu6vkSaND1hMcQ+jw+XMVhaoqESabq+lR5cQKBgQC2\nqWkykOl++jSK8O9QghOry00dDsT/y7Wv0n7gpiq/Eyv3Khgh2TssDlxSQz4GH/C7\n4jj3d6oIihObGHFNPH5HZvx9e9J9EOezMn0imT+/HT8FmbQTLvWFUeZF+dQSIMs8\nnWpYnv4tmiEuDhZ/x3lN27ciPveAkDS5W7qM9YrJ6QKBgQDHenfweOPuZxFmfqqG\nao/GuNLdCn8c2ZJzUJR5NNpaXnUlDxEgOmaeAw9quRNtdVAVHBnPC4uFegc7VBcA\n9guK7uF7Q1rB1Ypw8qzuEBz6BDQ9zc4q026xrPXADVK6zqC/CvRhbYaTgykZaCJl\nozRrvrZDHjjgC5wC2+d278NcFQ==\n-----END PRIVATE KEY-----\n", "client_email": "test-252@quickstart-1552345943126.iam.gserviceaccount.com", "client_id": "103778937997709997381", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test-252%40quickstart-1552345943126.iam.gserviceaccount.com" } +{ "type": "service_account", "project_id": "quickstart-1552345943126", "private_key_id": "1f029699545c3a00039b7ed0894f60d8bccfb970", "private_key": "-----BEGIN PRIVATE KEY-----\naVeryLongPrivateKey\n-----END PRIVATE KEY-----\n", "client_email": "test-252@quickstart-1552345943126.iam.gserviceaccount.com", "client_id": "103778937997709997381", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test-252%40quickstart-1552345943126.iam.gserviceaccount.com" } ``` 17. In the Spoke `.env` file, or the environment variables section in Heroku settings, or in whatever your platform uses for configuration, create a key called `GOOGLE_SECRET` and set its value to the single line of text you created in step 16. (If you're using a `.env` file you must surround it by single quotes. If you're using Heroku you don't need to add quotes.) For AWS Lambda, there are [special deployment instructions](HOWTO_DEPLOYING_AWS_LAMBDA.md#environment-variable-maximum-4k) diff --git a/docs/HOWTO_USE_POSTGRESQL.md b/docs/HOWTO_USE_POSTGRESQL.md index 92cd1f9e7..cff2f0787 100644 --- a/docs/HOWTO_USE_POSTGRESQL.md +++ b/docs/HOWTO_USE_POSTGRESQL.md @@ -3,7 +3,7 @@ To use Postgresql, follow these steps: 1. Either install docker (recommended) or postgresql on your machine: - * If you installed docker run the database using: `docker-compose up` + * If you installed docker run the database using: `docker compose up` * If you installed postgres locally, create the spoke dev database: `psql -c "create database spokedev;"` * Then create a spoke user to connect to the database with `createuser -P spoke` with password "spoke" (to match the credentials in the .env.example file) 1. In `.env` set `DB_TYPE=pg`. (Otherwise, you will use sqlite.) diff --git a/docs/REFERENCE-best-practices-conformance-messaging.md b/docs/REFERENCE-best-practices-conformance-messaging.md index 4ddcb4eaa..ede31905e 100644 --- a/docs/REFERENCE-best-practices-conformance-messaging.md +++ b/docs/REFERENCE-best-practices-conformance-messaging.md @@ -12,7 +12,7 @@ Spoke is a software application that facilitates individual texters sending indi Use of Spoke is subject to legal restrictions which each organization should review and understand, including recent guidance from an [FCC ruling](https://docs.fcc.gov/public/attachments/DA-20-670A1.pdf). Carriers and messaging services also take positions both on what is legally allowed and how they interpret messages sent through their services. Twilio documents [how carrier filtering works](https://support.twilio.com/hc/en-us/articles/223181848-How-Does-Carrier-Filtering-Work-), and a carrier industry group has published [CTIA guidelines](https://www.ctia.org/the-wireless-industry/industry-commitments/messaging-interoperability-sms-mms) that describe carrier perspectives. -MoveOn aggressively works to follow best practices to be legally compliant as well as a good citizen for texters and contact recipients. +StateVoices aggressively works to follow best practices to be legally compliant as well as a good citizen for texters and contact recipients. Please consult your own legal advice on how to deploy Spoke. Because Spoke is used by different organizations, in different countries, with different compliance laws and interpretations, Spoke allows for adjustment of the configuration options. Please consult with legal counsel as to how your organization should configure Spoke. @@ -57,7 +57,7 @@ You can modify what triggers an auto-optout by preparing JSON in the following s [{"regex": "^\\s*stop\\b|\\bremove me\\s*$|remove my name|\\btake me off th\\w+ list|\\blose my number|don\\W?t contact me|delete my number|I opt out|stop2quit|stopall|^\\s*unsubscribe\\s*$|^\\s*cancel\\s*$|^\\s*end\\s*$|^\\s*quit\\s*$", "reason": "stop"}] ``` -You can add multiple objects in the array to match different reasons. The default Spoke install does not include all the phrases MoveOn uses to trigger an auto-optout -- we also include offensive/hostile phrases, for example. +You can add multiple objects in the array to match different reasons. The default Spoke install does not include all the phrases StateVoices uses to trigger an auto-optout -- we also include offensive/hostile phrases, for example. ## Modifying the initial text message diff --git a/docs/REFERENCE-environment_variables.md b/docs/REFERENCE-environment_variables.md index 994544d15..644e8e67f 100644 --- a/docs/REFERENCE-environment_variables.md +++ b/docs/REFERENCE-environment_variables.md @@ -41,6 +41,7 @@ | DOWNTIME_NO_DB | On AWS Lambda this blocks the site from loading the app at all and swaps out a system that redirects users to /downtime. This is useful for DB maintenance. For non-Lambda environments, just run the src/server/downtime app instead of src/server/index default app | | DOWNTIME_TEXTER | Setting DOWNTIME_TEXTER to a text message (without quotes, please) will give the message as a text to texters when they arrive on the site, but the admin pages will still be accessible. This could be useful if you want to stop new texters from landing on the site and texting, while you debug things. | | DST_REFERENCE_TIMEZONE | Timezone to use to determine whether DST is in effect. If it's DST in this timezone, we assume it's DST everywhere. _Default_: "US/Eastern". (The default will work for any campaign in the US. For example, if the campaign is in Australia, use "Australia/Sydney" or some other timezone in Australia. Note that DST is opposite in the northern and souther hemispheres.) | +| DYNAMICASSIGNMENT_BATCHES | Enables texter strategies for campaigns. Enabled by default are "finished-replies-tz", "vetted-texters", and "finished-replies". Learn more at [HOWTO-use-dynamicassignment-batches.md](https://github.com/StateVoicesNational/Spoke/blob/e217a971f0e1eed6f96d1494aee0f62ce715a771/docs/HOWTO-use-dynamicassignment-batches.md#dynamic-assignment-strategies). NOTE: "vetted-takeconversations" is experimental and does not work on its own, needing other three default strategies enabled. | CONTACT_LOADERS | Optional contact loaders in [extensions/contact-loaders](https://github.com/StateVoicesNational/Spoke/tree/main/src/extensions/contact-loaders) _`ngpvan` Required for VAN integration_ | | EMAIL_FROM | Email from address. _Required to send email from either Mailgun **or** a custom SMTP server_. | | EMAIL_HOST | Email server host. _Required for custom SMTP server usage_. | diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md index db23fc126..87b98f591 100644 --- a/docs/RELEASE_NOTES.md +++ b/docs/RELEASE_NOTES.md @@ -1,5 +1,104 @@ # Release Notes +## v14.0 +_July 2024:_ Version 14.0 + +14.0 is a major release. This release upgrades Node 16 to Node 20. By upgrading to Node 20, Spoke is now stable and secure until Q1 of 2026. + +As this is StateVoices's first major release, we would like to express our gratitude to all contributors, MoveOn and its previous Community Managers, and the Progressive Coders Network for their continued support and belief in open-source software and Movement Technology. + +### Appreciations +[Maureen Zitouni](https://github.com/mau11), [Ruby Engelhart](https://github.com/engelhartrueben), [Arique Aguilar](https://github.com/Arique1104), [Larry Person](https://github.com/lperson), [Schuyler Duveen](https://github.com/schuyler1d), [Megan Brown](https://github.com/mabrownnyu), [Daniel Bond](https://github.com/dwbond), [Ilona Brand](https://github.com/ibrand), [Kathy Nguyen](https://github.com/crayolakat) + +### New Features + +There are no new features to this update. This update is purely for stability and security. + +### Highlights +- Upgraded Node 16 to Node 20 +- Updated apollo to use Apollo Client v3 and Apollo Server v4 +- Upgraded 48 and removed 15 packages +- Created GSIntegerField for integer form validation +- Fixed Google Docs integration for script imports + +### Packages: +| Package | Prev. Version | New Version | Notes | +| --- | --- | --- | --- | +| @apollo/client | ** | ^3.9.8 | Apollo Client v3; replaced older Apollo packages | +| @apolo/server | ** | ^4.10.1 | Apollo Server V4; replaced older Apollo packages | +| @aws-* | ** | ^3.535.0 | All packages were upgraded/implemented via AWS codemod. These changes were in response to upgrading aws-sdk to V4. | +| @babel/cli | ^7.19.3 | ^7.24.1 | *** | +| @babel/core | ^7.19.6 | ^7.24.1 | *** | +| @babel/plugin-syntax-flow | ** | ^7.14.5 | for unmet peer dependency :: react-scripts > eslint-config-react-app > eslint-plugin-flowtype@8.0.3 | +| @babel/plugin-transform-class-properties | ** | ^7.23.3 | *** | +| @babel/plugin-transform-nullish-coalescing-operator | ** | ^7.23.4 | *** | +| @babel/plugin-transform-optional-chaining | ** | ^7.23.4 | *** | +| @babel/plugin-transform-react-jsx | ** | ^7.14.9 | for unmet peer dependency :: react-scripts > eslint-config-react-app > eslint-plugin-flowtype@8.0.3 | +| @babel/plugin-transform-runtime | ^7.19.6 | ^7.24.3 | *** | +| @babel/preset-env | ^7.20.2 | ^7.23.9 | *** | +| @babel/register | ^7.18.9 | ^7.23.7 | *** | +| @babel/traverse | ** | 7.23.2 | *** | +| @graphql-tools/graphql-file-loader | ** | ^8.0.1 | *** | +| @graphql-tools/load | ** | ^8.0.2 | *** | +| @graphql-tools/schema | ** | ^10.0.3 | *** | +| auth0-js | ^9.14.3 | ^9.24.1 | *** | +| babel-loader | ^9.1.0 | ^9.1.3 | *** | +| bandwidth-sdk | ** | ^1.0.1 | Replaces @bandwidth/messaging | +| cookie-session | ^2.0.0-alpha.1 | ^2.1.0 | *** | +| dataloader | ^1.2.0 | ^2.2.2 | *** | +| eslint-plugin-react-hooks | ** | ^4.3.0 | *** | +| google-libphonenumber | ^3.0.0 | ^3.2.34 | *** | +| graphql | ^0.13.2 | ^16.8.1 | *** | +| graphql-type-json | ^0.1.4 | ^0.3.2 | *** | +| isomorphic-fetch | ^2.2.1 | ^3.0.0 | *** | +| knex | ^2.3.0 | ^3.1.0 | *** | +| moment-timezone | ^0.5.14 | ^0.5.45 | *** | +| nodemailer | ^6.4.16 | ^6.9.9 | *** | +| pg | ^8.0.3 | ^8.11.3 | *** | +| pg-query-stream | ^1.1.1 | ^4.5.3 | *** | +| redis | 3 | ^4.6.13 | *** | +| react-dnd | ** | 7.7.0 | *** | +| rollbar | ^2.4.4 | ^2.26.4 | *** | +| terser-webpack-plugin | 4 | ^5.3.10 | *** | +| twilio | ^3.40.0 | ^4.23.0 | *** | +| typescript | ** | ^3.2.1 | *** | +| webpack | ^5.74.0 | ^5.90.2 | *** | + +### Developer Packages +| Package | Prev. Version | New Version | Notes | +| --- | --- | --- | --- | +| @babel/eslint-parser | ^7.19.1 | ^7.24.1 | *** | +| enzyme-adapter-react-16 | ^1.15.6 | ^1.15.8 | *** | +| eslint | ^8.26.0 | ^8.56.0 | *** | +| eslint-plugin-import | ^2.26.0 | ^2.29.1 | *** | +| jest-when | ^3.4.1 | ^3.6.0 | *** | +| jest-environment-jsdom | ** | ^29.7.0 | *** | +| nock | 11.9.1 | 13.5.4 | *** | +| supertest | ^6.2.3 | ^6.3.4 | *** | +| webpack-cli | ^4.7.2 | ^4.10.0 | *** | + +_** New package_
+_*** Packages that were either upgraded due to a peer dependency conflict or because there was minimal to no changes to the code._ + +### Removed Packages +| Package | Reason | +| --- | --- | +| @bandwidth/messaging | Deprecated. Replaced by bandwidth-sdk | +| apollo-cache-inmemory | Removed to use Apollo Client v3 and Apollo Server v4 packages, @apollo/client and apollo/server in their place | +| apollo-client | Removed to use Apollo Client v3 and Apollo Server v4 packages, @apollo/client and apollo/server in their place | +| apollo-link | Removed to use Apollo Client v3 and Apollo Server v4 packages, @apollo/client and apollo/server in their place | +| apollo-link-error | Removed to use Apollo Client v3 and Apollo Server v4 packages, @apollo/client and apollo/server in their place | +| apollo-link-http | Removed to use Apollo Client v3 and Apollo Server v4 packages, @apollo/client and apollo/server in their place | +| apollo-server-express | Removed to use Apollo Client v3 and Apollo Server v4 packages, @apollo/client and apollo/server in their place | +| apollo-utilities | Removed to use Apollo Client v3 and Apollo Server v4 packages, @apollo/client and apollo/server in their place | +| camelcase-keys | Deprecated. Replaced with already used package humps | +| graphql-tools | Replaced by @graphql-tools/schema | +| node-abort-controller | Removed due to native support | +| url-join | Removed due to native support | +| react-apollo | Deprecated. Replaced with Apollo Client and flowRight from lodash | +| recompose | Deprecated. Required major refactor of many exports. | +| selenium-webdriver | Not used in the code base. | + ## v13.1.0 _October 2023_ Version 13.1.0 diff --git a/jest.config.js b/jest.config.js index 00f4a91b5..756e4e8d8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,7 +1,19 @@ +const esModules = [ + "bandwidth-sdk", + "axios", // bandwidth-sdk dependency + "filter-obj", // query-string dependency + "url-join", + "query-string", + "decode-uri-component", + "split-on-first" +]; + module.exports = { verbose: true, - testURL: "http://localhost:3000", testEnvironment: "node", + testEnvironmentOptions: { + url: "http://localhost:3000" + }, globals: { SUPPRESS_DATABASE_AUTOCREATE: "1", DB_JSON: JSON.stringify({ @@ -50,5 +62,11 @@ module.exports = { testPathIgnorePatterns: [ "/node_modules/", "/__test__/cypress/" - ] + ], + transformIgnorePatterns: esModules.length + ? [ + `/node_modules/(?!${esModules.join("|")})`, + "/node_modules/@aws-sdk/client-cloudwatch/package.json" + ] + : [] }; diff --git a/lambda.js b/lambda.js index 77cc22f58..b00f794e6 100644 --- a/lambda.js +++ b/lambda.js @@ -1,5 +1,4 @@ -"use strict"; -const AWS = require("aws-sdk"); +"use strict";; const awsServerlessExpress = require("aws-serverless-express"); let app, server, jobs, dispatcher; diff --git a/package.json b/package.json index 480488034..bb578f06e 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,18 @@ { "name": "spoke", - "version": "13.1.0", + "version": "14.0.0", "description": "Spoke", "main": "src/server", "engines": { - "node": ">=16.18.0", - "npm": "8.19.2" + "node": ">=20.11.1", + "npm": "10.2.4" }, "scripts": { "test": "jest --forceExit --runInBand --detectOpenHandles --verbose=false", "test-watch": "jest --watch --forceExit --runInBand --detectOpenHandles --verbose=false", "test-cache": "REDIS_FAKE=1 jest --runInBand --forceExit --detectOpenHandles", - "test-rediscache": "REDIS_URL=redis://localhost:6379 CACHE_PREFIX=test jest --runInBand --forceExit --detectOpenHandles", - "test-rediscache-contactcache": "REDIS_URL=redis://localhost:6379 CACHE_PREFIX=test REDIS_CONTACT_CACHE=1 jest --runInBand --forceExit --detectOpenHandles", + "test-rediscache": "REDIS_URL=redis://127.0.0.1:6379 CACHE_PREFIX=test jest --runInBand --forceExit --testPathIgnorePatterns=__test__/components/ __test__/containers __test__/cypress __test__/server/api/campaign/updateQuestionResponses.test.js --detectOpenHandles", + "test-rediscache-contactcache": "REDIS_URL=redis://127.0.0.1:6379 CACHE_PREFIX=test REDIS_CONTACT_CACHE=1 jest --runInBand --forceExit --testPathIgnorePatterns=__test__/components/ __test__/containers __test__/cypress __test__/server/api/campaign/updateQuestionResponses.test.js --detectOpenHandles", "test-cypress": "NODE_ENV=test DB_TYPE=pg DEFAULT_SERVICE=fakeservice SESSION_SECRET=secret DB_NAME=spoke_test DB_USER=spoke_test DB_PASSWORD=spoke_test cypress open", "test-sqlite": "jest --config jest.config.sqlite.js --runInBand --forceExit --detectOpenHandles", "test-coverage": "jest --coverage --detectOpenHandles --runInBand", @@ -81,65 +81,73 @@ "homepage": "https://github.com/StateVoicesNational/Spoke/#readme", "dependencies": { "@aoberoi/passport-slack": "^1.0.5", - "@babel/cli": "^7.19.3", - "@babel/core": "^7.19.6", + "@apollo/client": "^3.9.8", + "@apollo/server": "^4.10.1", + "@aws-sdk/client-cloudwatch": "^3.535.0", + "@aws-sdk/client-cloudwatch-events": "^3.535.0", + "@aws-sdk/client-lambda": "^3.535.0", + "@aws-sdk/client-s3": "^3.535.0", + "@aws-sdk/client-sqs": "^3.535.0", + "@aws-sdk/s3-request-presigner": "^3.535.0", + "@babel/cli": "^7.24.1", + "@babel/core": "^7.24.1", "@babel/plugin-proposal-export-default-from": "^7.18.10", - "@babel/plugin-transform-runtime": "^7.19.6", - "@babel/preset-env": "^7.20.2", + "@babel/plugin-syntax-flow": "^7.14.5", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", + "@babel/plugin-transform-react-jsx": "^7.14.9", + "@babel/plugin-transform-runtime": "^7.24.3", + "@babel/preset-env": "^7.23.9", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.18.6", - "@babel/register": "^7.18.9", - "@bandwidth/messaging": "^3.0.0", + "@babel/register": "^7.23.7", + "@babel/traverse": "7.23.2", "@bandwidth/numbers": "^1.7.0", "@date-io/core": "^1.3.13", "@date-io/date-fns": "^1.3.13", + "@graphql-tools/graphql-file-loader": "^8.0.1", + "@graphql-tools/load": "^8.0.2", + "@graphql-tools/schema": "^10.0.3", "@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", - "auth0-js": "^9.14.3", + "auth0-js": "^9.24.1", "aws-serverless-express": "^3.3.6", - "babel-loader": "^9.1.0", + "babel-loader": "^9.1.3", "babel-polyfill": "^6.26.0", "babel-runtime": "^6.26.0", + "bandwidth-sdk": "^1.0.1", "body-parser": "^1.15.2", "browserify": "^17.0.0", - "camelcase-keys": "^4.1.0", "color-difference": "^0.3.4", - "cookie-session": "^2.0.0-alpha.1", - "dataloader": "^1.2.0", + "cookie-session": "^2.1.0", + "dataloader": "^2.2.2", "dotenv": "^2.0.0", + "eslint-plugin-react-hooks": "^4.3.0", "express": "^4.14.0", "fs": "^0.0.2", - "google-libphonenumber": "^3.0.0", + "google-libphonenumber": "^3.2.34", "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-type-json": "^0.3.2", "heroku-ssl-redirect": "^0.1.1", "humps": "^1.1.0", "is-url": "^1.2.2", - "isomorphic-fetch": "^2.2.1", + "isomorphic-fetch": "^3.0.0", "jest-each": "^0.3.1", "json-loader": "^0.5.4", "jsonwebtoken": "^9.0.0", - "knex": "^2.3.0", + "knex": "^3.1.0", "lodash": "^4.13.1", "mailgun-js": "^0.20.0", "minilog": "^3.0.1", "moment": "2.29.4", - "moment-timezone": "^0.5.14", + "moment-timezone": "^0.5.45 ", "nexmo": "^2.3.2", - "node-abort-controller": "^1.0.4", "node-fetch": "^2.6.7", - "nodemailer": "^6.4.16", + "nodemailer": "^6.9.9", "omit-deep-lodash": "^1.1.4", "papaparse": "^5.1.1", "passport": "^0.3.2", @@ -147,62 +155,63 @@ "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", "passport-local-authenticate": "^1.2.0", - "pg": "^8.0.3", + "pg": "^8.11.3", "pg-connection-string": "^2.4.0", - "pg-query-stream": "^1.1.1", + "pg-query-stream": "^4.5.3", "prop-types": "^15.6.0", - "query-string": "^4.1.0", + "query-string": "^9.0.0", "react": "16.14.0", - "redis": "3", + "react-dnd": "7.7.0", + "redis": "^4.6.13", "request": "^2.81.0", "rethink-knex-adapter": "0.4.20", - "rollbar": "^2.4.4", - "terser-webpack-plugin": "4", + "rollbar": "^2.26.4", + "terser-webpack-plugin": "^5.3.10", "thinky": "^2.3.3", "timezonecomplete": "^5.5.0", - "twilio": "^3.40.0", - "url-join": "^4.0.1", + "twilio": "^4.23.0", + "typescript": "^3.2.1", "us-area-codes": "^1.0.0", "uuid": "^9.0.0", "wait-for-expect": "^1.1.1", - "webpack": "^5.74.0", + "webpack": "^5.90.2", "webpack-manifest-plugin": "^5.0.0", "yup": "0.32.3" }, "devDependencies": { - "@babel/eslint-parser": "^7.19.1", + "@babel/eslint-parser": "^7.24.1", "babel-jest": "^29.3.1", "babel-preset-es2017": "^6.24.1", "cypress": "5.6.0", "cypress-file-upload": "^4.0.6", "cypress-wait-until": "^1.7.1", "enzyme": "^3.11.0", - "enzyme-adapter-react-16": "^1.15.6", - "eslint": "^8.26.0", + "enzyme-adapter-react-16": "^1.15.8", + "eslint": "^8.56.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^6.10.0", - "eslint-plugin-import": "^2.26.0", + "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-react": "^7.31.10", "fakeredis": "^2.0.0", "foreman": "^3.0.1", "husky": "4", - "jest-when": "^3.4.1", + "jest-environment-jsdom": "^29.7.0", + "jest-when": "^3.6.0", "json2csv": "^3.6.2", "lint-staged": "^9.2.0", "mockdate": "^2.0.2", - "nock": "11.9.1", + "nock": "13.5.4", "nodemon": "^2.0.2", "prettier": "^1.18.2", "react-hot-loader": "4.13.0", "react-scripts": "^5.0.1", "react-test-renderer": "16", "regenerator-runtime": "^0.10.5", - "selenium-webdriver": "^3.6.0", "sqlite3": "^5.1.2", - "supertest": "^6.2.3", + "supertest": "^6.3.4", "wait-on": "^2.1.0", - "webpack-cli": "^4.7.2", + "webpack-cli": "^4.10.0", "webpack-dev-server": "3" }, "lint-staged": { @@ -222,15 +231,17 @@ "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", "react-dom": "16.14.0", "react-formal": "2.2.2", "react-router": "^3.2.0", - "react-tooltip": "^4.2.13", - "recompose": "^0.30.0", - "webpack-cli": "^4.7.2" + "react-tooltip": "^4.2.13" + }, + "resolutions": { + "strip-ansi": "6.0.1", + "axios": "0.28.0", + "jest": "^29.7.0" } } 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..c84b7cef2 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"; 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/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/AssignmentSummary.jsx b/src/components/AssignmentSummary.jsx index 3cee8d080..99412b457 100644 --- a/src/components/AssignmentSummary.jsx +++ b/src/components/AssignmentSummary.jsx @@ -1,7 +1,6 @@ import PropTypes from "prop-types"; import React, { Component } from "react"; import { StyleSheet, css } from "aphrodite"; -import { compose } from "recompose"; import Button from "@material-ui/core/Button"; import Badge from "@material-ui/core/Badge"; @@ -281,4 +280,4 @@ AssignmentSummaryBase.propTypes = { todoLink: PropTypes.func }; -export default compose(withMuiTheme, withRouter)(AssignmentSummaryBase); +export default withMuiTheme(withRouter(AssignmentSummaryBase)); diff --git a/src/components/AssignmentTexter/Controls.jsx b/src/components/AssignmentTexter/Controls.jsx index fe47ed1bd..d1b88ce85 100644 --- a/src/components/AssignmentTexter/Controls.jsx +++ b/src/components/AssignmentTexter/Controls.jsx @@ -1,7 +1,6 @@ import PropTypes from "prop-types"; import React from "react"; import { css } from "aphrodite"; -import { compose } from "recompose"; import Toolbar from "./Toolbar"; import MessageList from "./MessageList"; import Survey from "./Survey"; @@ -36,6 +35,7 @@ import { import { dataTest } from "../../lib/attributes"; import ContactToolbar from "./ContactToolbar"; import { getCookie, setCookie } from "../../lib/cookie"; +import { deepCopy } from "../utils"; export class AssignmentTexterContactControls extends React.Component { constructor(props) { @@ -125,7 +125,10 @@ export class AssignmentTexterContactControls extends React.Component { let currentInteractionStep = null; if (availableSteps.length > 0) { - currentInteractionStep = availableSteps[availableSteps.length - 1]; + const currentInteractionStep = deepCopy( + 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 ? ( @@ -880,9 +883,9 @@ export class AssignmentTexterContactControls extends React.Component { shortCannedResponses = shortCannedResponses.filter(script => { 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 {