Skip to content

Commit

Permalink
Merge branch 'lp_fix_convo_link' into lp_zapier
Browse files Browse the repository at this point in the history
  • Loading branch information
lperson committed Jun 24, 2020
2 parents f4413ca + 1af03e0 commit 24b2671
Show file tree
Hide file tree
Showing 60 changed files with 2,849 additions and 1,751 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/jest-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,37 @@ jobs:
${{ runner.os }}-${{ matrix.node-version }}-yarn-
- run: yarn
- run: yarn test
test-rediscache-contactcache:
runs-on: ubuntu-latest
timeout-minutes: 10
services:
redis:
image: redis
ports:
- 6379:6379
postgres:
image: postgres:10
env:
POSTGRES_USER: spoke_test
POSTGRES_PASSWORD: spoke_test
POSTGRES_DB: spoke_test
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- uses: actions/checkout@v2
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- run: yarn
- run: yarn test-rediscache-contactcache
test-rediscache:
runs-on: ubuntu-latest
timeout-minutes: 10
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ 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 [6.0](https://github.com/MoveOnOrg/Spoke/tree/v6.0) (see [release notes](https://github.com/MoveOnOrg/Spoke/blob/main/docs/RELEASE_NOTES.md#v60))
The latest version is [7.0](https://github.com/MoveOnOrg/Spoke/tree/v7.0) (see [release notes](https://github.com/MoveOnOrg/Spoke/blob/main/docs/RELEASE_NOTES.md#v70))

## Deploy to Heroku

Use the Heroku Button to deploy a version of Spoke suitable for testing. This won't cost any money and will not support production usage. It's a great way to practice deploying Spoke or see it in action.
<a href="https://heroku.com/deploy?template=https://github.com/MoveOnOrg/Spoke/tree/v6.0">
<a href="https://heroku.com/deploy?template=https://github.com/MoveOnOrg/Spoke/tree/v7.0">
<img src="https://www.herokucdn.com/deploy/button.svg" alt="Deploy">
</a>

Or click [this link to deploy with a prod infrastructure set up to get up and running!](https://heroku.com/deploy?template=https://github.com/MoveOnOrg/Spoke/tree/heroku-button-paid)

**NOTE:** Deploying with prod infrastructure will cost $75 ($25 dyno + $50 postgres) a month and should be suitable for production level usage for most organizations.

Follow up instructions located [here](https://github.com/MoveOnOrg/Spoke/blob/main/docs/HOWTO_HEROKU_DEPLOY.md).
Expand Down
30 changes: 1 addition & 29 deletions __test__/backend.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { resolvers } from "../src/server/api/schema";
import { schema } from "../src/api/schema";
import { assignmentRequired } from "../src/server/api/errors";
import { assignmentRequiredOrAdminRole } from "../src/server/api/errors";
import { graphql } from "graphql";

console.log("This is an intentional error");
Expand Down Expand Up @@ -501,34 +501,6 @@ describe("graphql test suite", async () => {
);
expect(results).toEqual(false);
});

test("test assignmentRequired access control", async () => {
const user = await createUser();

const assignment = await new Assignment({
user_id: user.id,
campaign_id: campaign.id
}).save();

const allowUser = await assignmentRequired(
user,
assignment.id,
assignment
);
expect(allowUser).toEqual(true);
const allowUserAssignmentId = await assignmentRequired(
user,
assignment.id
);
expect(allowUserAssignmentId.user_id).toEqual(user.id);
expect(allowUserAssignmentId.id).toEqual(assignment.id);
try {
const notAllowed = await assignmentRequired(user, -1);
throw new Exception("should throw BEFORE this exception");
} catch (err) {
expect(/not authorized/.test(String(err))).toEqual(true);
}
});
});

describe("Copy Campaign", () => {
Expand Down
11 changes: 9 additions & 2 deletions __test__/containers/UserMenu.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ describe("UserMenu", () => {
id: 1,
displayName: "TestName",
email: "[email protected]",
organizations: [
superVolOrganizations: [
{
id: 2,
name: "testOrg"
}
],
texterOrganizations: [
{
id: 2,
name: "testOrg"
Expand Down Expand Up @@ -46,7 +52,8 @@ describe("UserMenu", () => {
id: 1,
displayName: "TestName",
email: "[email protected]",
organizations: [
superVolOrganizations: [],
texterOrganizations: [
{
id: 2,
name: "testOrg"
Expand Down
177 changes: 177 additions & 0 deletions __test__/integrations/message-handlers/profanity-tagger.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { r, cacheableData } from "../../../src/server/models";
import {
postMessageSave,
available,
DEFAULT_PROFANITY_REGEX_BASE64
} from "../../../src/integrations/message-handlers/profanity-tagger";

import {
setupTest,
cleanupTest,
createStartedCampaign,
sendMessage
} from "../../test_helpers";

beforeEach(async () => {
// Set up an entire working campaign
await setupTest();
global.MESSAGE_HANDLERS = "profanity-tagger";
}, global.DATABASE_SETUP_TEARDOWN_TIMEOUT);

afterEach(async () => {
await cleanupTest();
if (r.redis) r.redis.flushdb();
global.MESSAGE_HANDLERS = undefined;
}, global.DATABASE_SETUP_TEARDOWN_TIMEOUT);

describe("Message Hanlder: profanity-tagger", () => {
it("default regex works", () => {
const re = new RegExp(
Buffer.from(DEFAULT_PROFANITY_REGEX_BASE64, "base64").toString(),
"i"
);
expect(re.test("blah blah fakeslur blah blah")).toBe(true);
expect("brass shoe eddie homonym Saturday".match(re)).toBe(null);
});

it("Contact profanity is flagged", async () => {
// SETUP
const c = await createStartedCampaign();
await r.knex("tag").insert([
{
name: "Contact Profanity",
description: "mean contact",
organization_id: c.organizationId
},
{
name: "Texter language flag",
description: "texter inappropriate",
organization_id: c.organizationId
}
]);
await r
.knex("organization")
.update(
"features",
'{"EXPERIMENTAL_TAGS": "1", "PROFANITY_CONTACT_TAG_ID": "1", "PROFANITY_TEXTER_TAG_ID": "2", "PROFANITY_TEXTER_SUSPEND_COUNT": "1"}'
);
await cacheableData.organization.clear(c.organizationId);
const org = await cacheableData.organization.load(c.organizationId);
await sendMessage(c.testContacts[1].id, c.testTexterUser, {
userId: c.testTexterUser.id,
contactNumber: c.testContacts[1].cell,
text: "brass shoe eddie homonym",
assignmentId: c.assignmentId
});
// a little stupidly updating messageservice_sid is necessary
// because it's not await'd
await r
.knex("message")
.where("user_id", c.testTexterUser.id)
.update("messageservice_sid", "fakeservice");
await cacheableData.message.save({
contact: c.testContacts[1],
messageInstance: {
is_from_contact: true,
text: "go to fakeslur!",
contact_number: c.testContacts[1].cell,
service: "fakeservice",
messageservice_sid: "fakeservice",
send_status: "DELIVERED"
}
});

const text1 = await r
.knex("tag_campaign_contact")
.select("tag_id", "campaign_contact_id");
expect(text1).toEqual([{ tag_id: 1, campaign_contact_id: 2 }]);
const user = await cacheableData.user.userHasRole(
c.testTexterUser,
c.organizationId,
"TEXTER"
);
expect(user).toBe(true);
});

it("Texter profanity is flagged", async () => {
// SETUP
const c = await createStartedCampaign();
await r.knex("tag").insert([
{
name: "Contact Profanity",
description: "mean contact",
organization_id: c.organizationId
},
{
name: "Texter language flag",
description: "texter inappropriate",
organization_id: c.organizationId
}
]);
await r
.knex("organization")
.update(
"features",
'{"EXPERIMENTAL_TAGS": "1", "PROFANITY_CONTACT_TAG_ID": "1", "PROFANITY_TEXTER_TAG_ID": "2", "PROFANITY_TEXTER_SUSPEND_COUNT": "2"}'
);
await cacheableData.organization.clear(c.organizationId);
const org = await cacheableData.organization.load(c.organizationId);

// Confirm Available
expect(available(org)).toBeTruthy();

// Confirm texter catch
await sendMessage(c.testContacts[0].id, c.testTexterUser, {
userId: c.testTexterUser.id,
contactNumber: c.testContacts[0].cell,
text: "Some fakeslur message",
assignmentId: c.assignmentId
});
const text1 = await r
.knex("tag_campaign_contact")
.select("tag_id", "campaign_contact_id")
.where("campaign_contact_id", 1);
expect(text1).toEqual([{ tag_id: 2, campaign_contact_id: 1 }]);

let user = await cacheableData.user.userHasRole(
c.testTexterUser,
c.organizationId,
"TEXTER"
);
expect(user).toBe(true);

// Confirm texter no-match
await sendMessage(c.testContacts[1].id, c.testTexterUser, {
userId: c.testTexterUser.id,
contactNumber: c.testContacts[1].cell,
text: "brass shoe eddie homonym",
assignmentId: c.assignmentId
});
const text2 = await r
.knex("tag_campaign_contact")
.select("tag_id", "campaign_contact_id")
.where("campaign_contact_id", 2);
expect(text2).toEqual([]);

user = await cacheableData.user.userHasRole(
c.testTexterUser,
c.organizationId,
"TEXTER"
);
expect(user).toBe(true);

// Confirm texter no-match
await sendMessage(c.testContacts[1].id, c.testTexterUser, {
userId: c.testTexterUser.id,
contactNumber: c.testContacts[1].cell,
text: "fakeslur is one too many slurs",
assignmentId: c.assignmentId
});
user = await cacheableData.user.userHasRole(
c.testTexterUser,
c.organizationId,
"TEXTER"
);
expect(user).toBe(false);
});
});
15 changes: 4 additions & 11 deletions __test__/server/api/campaign/campaign.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import gql from "graphql-tag";
import { r } from "../../../../src/server/models";
import { getConfig } from "../../../../src/server/api/lib/config";
import { dataQuery as TexterTodoListQuery } from "../../../../src/containers/TexterTodoList";
import { dataQuery as TexterTodoQuery } from "../../../../src/containers/TexterTodo";
import { campaignDataQuery as AdminCampaignEditQuery } from "../../../../src/containers/AdminCampaignEdit";
Expand Down Expand Up @@ -417,7 +418,7 @@ it("should save campaign canned responses across copies and match saved data", a
});

describe("Caching", async () => {
if (r.redis) {
if (r.redis && getConfig("REDIS_CONTACT_CACHE")) {
it("should not have any selects on a cached campaign when message sending", async () => {
await createScript(testAdminUser, testCampaign);
await startCampaign(testAdminUser, testCampaign);
Expand Down Expand Up @@ -583,7 +584,6 @@ describe("Reassignments", async () => {
text: "test text autorespond",
assignmentId: assignmentId2
});
console.log("campaign.test sendMessage", messageRes);
}
// does this sleep fix the "sometimes 4 instead of 5" below?
await sleep(5);
Expand All @@ -601,10 +601,6 @@ describe("Reassignments", async () => {
},
testTexterUser2
);
console.log(
"campaign.test texterCampaignDataResults.data needsMessage",
JSON.stringify(texterCampaignDataResults.data, null, 2)
);
expect(texterCampaignDataResults.data.assignment.contacts.length).toEqual(
15
);
Expand All @@ -623,10 +619,6 @@ describe("Reassignments", async () => {
},
testTexterUser2
);
console.log(
"campaign.test texterCampaignDataResults.data needsResponse",
JSON.stringify(texterCampaignDataResults.data, null, 2)
);
// often is sometimes 4 instead of 5 in test results. WHY?!!?!?
expect(texterCampaignDataResults.data.assignment.contacts.length).toEqual(
5
Expand Down Expand Up @@ -794,14 +786,15 @@ describe("Reassignments", async () => {
bulkReassignCampaignContactsMutation,
{
organizationId,
newTexterUserId: testTexterUser.id,
contactsFilter: {
messageStatus: "needsResponse",
isOptedOut: false,
validTimezone: true
},
campaignsFilter: { campaignId: testCampaign.id },
assignmentsFilter: { texterId: testTexterUser2.id },
newTexterUserId: testTexterUser.id
messageTextFilter: ""
},
testAdminUser
);
Expand Down
Loading

0 comments on commit 24b2671

Please sign in to comment.