diff --git a/.env.example b/.env.example index 883c41856..9564b79b6 100644 --- a/.env.example +++ b/.env.example @@ -52,4 +52,14 @@ TEXTER_SIDEBOXES=celebration-gif,default-dynamicassignment,default-releasecontac OWNER_CONFIGURABLE=ALL NGP_VAN_API_KEY= NGP_VAN_APP_NAME= -NGP_VAN_WEBHOOK_BASE_URL= \ No newline at end of file +NGP_VAN_WEBHOOK_BASE_URL= +NGP_VAN_API_KEY_ENCRYPTED= +NGP_VAN_API_BASE_URL= +NGP_VAN_CACHE_TTL= +NGP_VAN_EXPORT_JOB_TYPE_ID= +NGP_VAN_MAXIMUM_LIST_SIZE= +NGP_VAN_CAUTIOUS_CELL_PHONE_SELECTION= +ACTION_HANDLERS= +MESSAGE_HANDLERS= +CONTACT_LOADERS= +SERVICE_MANAGERS= \ No newline at end of file diff --git a/README.md b/README.md index 34da8a9bc..456f27d40 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Spoke is an open source text-distribution tool for organizations to mobilize sup Spoke was created by Saikat Chakrabarti and Sheena Pakanati, and is now maintained by MoveOn.org. -The latest version is [13.0.1](https://github.com/MoveOnOrg/Spoke/tree/13.0.1) (see [release notes](https://github.com/MoveOnOrg/Spoke/blob/main/docs/RELEASE_NOTES.md#v1301)) +The latest version is [13.1.0](https://github.com/MoveOnOrg/Spoke/tree/13.1.0) (see [release notes](https://github.com/MoveOnOrg/Spoke/blob/main/docs/RELEASE_NOTES.md#v1310)) ## Setting up Spoke @@ -24,7 +24,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 diff --git a/__test__/components/AssignmentSummary.test.js b/__test__/components/AssignmentSummary.test.js index f12345339..1d806e6a2 100644 --- a/__test__/components/AssignmentSummary.test.js +++ b/__test__/components/AssignmentSummary.test.js @@ -22,6 +22,9 @@ function getAssignment({ isDynamic = false, counts = {} }) { id: "1", title: "New Campaign", description: "asdf", + organization: { + allowSendAll: window.ALLOW_SEND_ALL + }, useDynamicAssignment: isDynamic, hasUnassignedContacts: false, introHtml: "yoyo", @@ -79,7 +82,7 @@ describe("AssignmentSummary text", function t() { ); }); -describe("AssignmentSummary actions inUSA and NOT AllowSendAll", () => { +describe("AssignmentSummary actions when NOT AllowSendAll", () => { function create( unmessaged, unreplied, @@ -88,7 +91,6 @@ describe("AssignmentSummary actions inUSA and NOT AllowSendAll", () => { skipped, isDynamic ) { - window.NOT_IN_USA = 0; window.ALLOW_SEND_ALL = false; return mount( @@ -185,7 +187,7 @@ describe("AssignmentSummary actions inUSA and NOT AllowSendAll", () => { }); }); -describe("AssignmentSummary NOT inUSA and AllowSendAll", () => { +describe("AssignmentSummary when AllowSendAll", () => { function create( unmessaged, unreplied, @@ -194,7 +196,6 @@ describe("AssignmentSummary NOT inUSA and AllowSendAll", () => { skipped, isDynamic ) { - window.NOT_IN_USA = 1; window.ALLOW_SEND_ALL = true; return mount( { ).toBe("Send messages"); }); - it('renders "Send messages" with unreplied', () => { + it('renders "Respond" with unreplied', () => { const actions = create(0, 1, 0, 0, 0, false); expect( actions .find(Button) .at(0) .text() - ).toBe("Send messages"); + ).toBe("Respond"); }); }); @@ -328,17 +329,21 @@ describe("contacts filters", () => { })} /> ); - const sendMessages = mockRender.mock.calls[0][0]; + const respondMessages = mockRender.mock.calls[0][0]; + expect(respondMessages.title).toBe("Respond"); + expect(respondMessages.contactsFilter).toBe("reply"); + + const sendMessages = mockRender.mock.calls[1][0]; expect(sendMessages.title).toBe("Past Messages"); expect(sendMessages.contactsFilter).toBe("stale"); - const skippedMessages = mockRender.mock.calls[1][0]; + const skippedMessages = mockRender.mock.calls[2][0]; expect(skippedMessages.title).toBe("Skipped Messages"); expect(skippedMessages.contactsFilter).toBe("skipped"); - const sendFirstTexts = mockRender.mock.calls[2][0]; + const sendFirstTexts = mockRender.mock.calls[3][0]; expect(sendFirstTexts.title).toBe("Send messages"); - expect(sendFirstTexts.contactsFilter).toBe("all"); + expect(sendFirstTexts.contactsFilter).toBe("text"); }); }); diff --git a/__test__/extensions/contact-loaders/ngpvan/ngpvan.test.js b/__test__/extensions/contact-loaders/ngpvan/ngpvan.test.js index be96dab56..c108bf912 100644 --- a/__test__/extensions/contact-loaders/ngpvan/ngpvan.test.js +++ b/__test__/extensions/contact-loaders/ngpvan/ngpvan.test.js @@ -469,6 +469,7 @@ describe("ngpvan", () => { ["NGP_VAN_API_BASE_URL", organization], ["NGP_VAN_TIMEOUT", organization], ["NGP_VAN_APP_NAME", organization], + ["NGP_VAN_API_KEY_ENCRYPTED", organization, {}], ["NGP_VAN_API_KEY", organization], ["NGP_VAN_DATABASE_MODE", organization], ["NGP_VAN_EXPORT_JOB_TYPE_ID", organization], diff --git a/__test__/extensions/contact-loaders/ngpvan/util.test.js b/__test__/extensions/contact-loaders/ngpvan/util.test.js index d2d72fb13..261d4e803 100644 --- a/__test__/extensions/contact-loaders/ngpvan/util.test.js +++ b/__test__/extensions/contact-loaders/ngpvan/util.test.js @@ -120,12 +120,13 @@ describe("ngpvan/util", () => { } try { - auth = Van.getAuth(organization); + auth = await Van.getAuth(organization); } catch (caughtException) { error = caughtException; } finally { expect(config.getConfig.mock.calls).toEqual([ ["NGP_VAN_APP_NAME", organization], + ["NGP_VAN_API_KEY_ENCRYPTED", organization, {}], ["NGP_VAN_API_KEY", organization], ["NGP_VAN_DATABASE_MODE", organization] ]); diff --git a/__test__/extensions/service-vendors/twilio.test.js b/__test__/extensions/service-vendors/twilio.test.js index 7d4485f67..cea2f4079 100644 --- a/__test__/extensions/service-vendors/twilio.test.js +++ b/__test__/extensions/service-vendors/twilio.test.js @@ -849,7 +849,6 @@ describe("config functions", () => { ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts] ]); expect(configFunctions.getConfig.mock.calls).toEqual([ - ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], ["TWILIO_AUTH_TOKEN_ENCRYPTED", organization, expectedConfigOpts], ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], ["TWILIO_ACCOUNT_SID", organization, expectedConfigOpts], diff --git a/__test__/server/api/campaign/campaign.test.js b/__test__/server/api/campaign/campaign.test.js index 7dd6f5a7b..9884eabbb 100644 --- a/__test__/server/api/campaign/campaign.test.js +++ b/__test__/server/api/campaign/campaign.test.js @@ -863,7 +863,8 @@ describe("Bulk Send", () => { ) => { process.env.ALLOW_SEND_ALL = params.allowSendAll; process.env.ALLOW_SEND_ALL_ENABLED = params.allowSendAllEnabled; - process.env.BULK_SEND_CHUNK_SIZE = params.bulkSendChunkSize; + process.env.BULK_SEND_BATCH_SIZE = + params.bulkSendBatchSize || params.bulkSendChunkSize; testCampaign.use_dynamic_assignment = true; await createScript(testAdminUser, testCampaign); @@ -1014,6 +1015,20 @@ describe("Bulk Send", () => { }; await testBulkSend(params, 0, expectErrorBulkSending); }); + + it("should send initial texts to as many contacts as are in the batch size if batch size is smaller than the chunk size", async () => { + const params = { + allowSendAll: true, + allowSendAllEnabled: true, + bulkSendBatchSize: Math.round(NUMBER_OF_CONTACTS / 4), + bulkSendChunkSize: NUMBER_OF_CONTACTS + }; + await testBulkSend( + params, + params.bulkSendBatchSize, + expectSuccessBulkSending(params.bulkSendBatchSize) + ); + }); }); describe("campaigns query", () => { diff --git a/__test__/server/api/people.test.js b/__test__/server/api/people.test.js index 3496f4209..beeb84700 100644 --- a/__test__/server/api/people.test.js +++ b/__test__/server/api/people.test.js @@ -1,7 +1,12 @@ +/** + * @jest-environment jsdom + */ /* 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 { resolvers } from "../../../src/server/api/schema"; +import { validate as uuidValidate } from 'uuid'; import { setupTest, @@ -479,4 +484,49 @@ describe("people", () => { ]); }); }); + + describe("reset password", () => { + /** + * Run the resetUserPassword mutation + * @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]; + } + } + }, + user: admin + }); + } + + it("reset local password", () => { + resetUserPassword(testAdminUsers[0], organizationId, testTexterUsers[0].id).then(uuid => { + // Non-Auth0 password reset will return verion 4 UUID + expect(uuidValidate(uuid)).toBeTruthy(); + }); + }); + + 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; + + 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/); + + expect(match).toHaveLength(2); + expect(uuidValidate(match[1])).toBeTruthy(); + }); + }); + }); }); diff --git a/app.json b/app.json index 9495516b8..a52f7d49e 100644 --- a/app.json +++ b/app.json @@ -44,19 +44,18 @@ }, "DEFAULT_SERVICE": { - "description": "specifies using twilio api ", - "required": true, - "value": "twilio" + "description": "type bandwidth or twilio", + "required": true }, "TWILIO_ACCOUNT_SID": { "description": "for twilio integration and connected to twilio account", - "required": true + "required": false }, "TWILIO_AUTH_TOKEN": { "description": "for twilio integration and connected to twilio account", - "required": true + "required": false }, "TWILIO_MULTI_ORG": { @@ -184,9 +183,9 @@ } }, "addons": [ - "heroku-postgresql:hobby-dev", + "heroku-postgresql:basic", { - "plan": "heroku-redis:hobby-dev", + "plan": "heroku-redis:mini", "options": { "maxmemory-policy": "volatile-lru" } diff --git a/docs/HOWTO-configure-auth0.md b/docs/HOWTO-configure-auth0.md index 6e01eb44d..381fed832 100644 --- a/docs/HOWTO-configure-auth0.md +++ b/docs/HOWTO-configure-auth0.md @@ -51,7 +51,7 @@ callback(null, user, context); - +