mirror of
https://fast.feibisi.com/https://github.com/parcelvoy/platform.git
synced 2025-08-29 11:56:04 +08:00
Add Github action to run jest tests (#20)
* Add Github action to run jest tests * Change how DB is create * Starts adding campaign tests * Fixes imports * Fixes spy/mocks for test cases * Tweaks when tests are run
This commit is contained in:
parent
2514dcc53f
commit
2daee6127c
9 changed files with 315 additions and 17 deletions
62
.github/workflows/test.yml
vendored
62
.github/workflows/test.yml
vendored
|
@ -1,6 +1,7 @@
|
|||
name: Build
|
||||
on:
|
||||
pull_request:
|
||||
name: Test
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -20,4 +21,57 @@ jobs:
|
|||
npm install
|
||||
- name: Lint
|
||||
run: |
|
||||
npm run lint
|
||||
npm run lint
|
||||
|
||||
test:
|
||||
needs: lint
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0.27
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
|
||||
MYSQL_DATABASE: 'parcelvoy'
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: >-
|
||||
--tmpfs="/var/lib/mysql"
|
||||
--health-cmd="mysqladmin ping"
|
||||
--health-interval=10s
|
||||
--health-timeout=5s
|
||||
--health-retries=3
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Node 16.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: Cache NPM
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install && npm install --save-dev
|
||||
|
||||
- name: 'Run Jest Tests'
|
||||
run: npm test
|
||||
env:
|
||||
NODE_ENV: test
|
||||
APP_SECRET: ${{ secrets.APP_SECRET }}
|
||||
DB_CLIENT: mysql2
|
||||
DB_DATABASE: parcelvoy
|
||||
DB_USERNAME: root
|
||||
DB_PORT: 3306
|
||||
APP_BASE_URL: https://parcelvoy.com
|
||||
QUEUE_DRIVER: memory
|
||||
|
|
|
@ -41,7 +41,6 @@ exports.up = function(knex) {
|
|||
.onDelete('CASCADE')
|
||||
table.integer('list_id')
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references('id')
|
||||
.inTable('lists')
|
||||
.onDelete('CASCADE')
|
||||
|
|
|
@ -28,9 +28,12 @@ export const createCampaign = async (params: CampaignParams): Promise<Campaign>
|
|||
})
|
||||
}
|
||||
|
||||
export function sendCampaign(campaign: Campaign, user: User, event?: UserEvent): Promise<void>
|
||||
export function sendCampaign(campaign: Campaign, userId: number, eventId?: number): Promise<void>
|
||||
export async function sendCampaign(campaign: Campaign, user: User | number, event?: UserEvent | number): Promise<void> {
|
||||
type SendCampaign = {
|
||||
(campaign: Campaign, user: User, event?: UserEvent): Promise<void>,
|
||||
(campaign: Campaign, userId: number, eventId?: number): Promise<void>,
|
||||
}
|
||||
|
||||
export const sendCampaign: SendCampaign = async (campaign: Campaign, user: User | number, event?: UserEvent | number): Promise<void> => {
|
||||
|
||||
// TODO: Might also need to check for unsubscribe in here since we can
|
||||
// do individual sends
|
||||
|
@ -72,9 +75,10 @@ export const recipientQuery = (campaign: Campaign) => {
|
|||
// Merge user subscription state in to filter out anyone
|
||||
// who we can't send do
|
||||
return UserList.query()
|
||||
.select('user_list.user_id')
|
||||
.where('list_id', campaign.list_id)
|
||||
.leftJoin('user_subscription', qb => {
|
||||
qb.on('lists.user_id', 'user_subscription.user_id')
|
||||
qb.on('user_list.user_id', 'user_subscription.user_id')
|
||||
.andOn('user_subscription.subscription_id', '=', UserList.raw(campaign.subscription_id))
|
||||
})
|
||||
.where(qb => {
|
||||
|
|
226
src/campaigns/__tests__/CampaignService.spec.ts
Normal file
226
src/campaigns/__tests__/CampaignService.spec.ts
Normal file
|
@ -0,0 +1,226 @@
|
|||
import App from '../../app'
|
||||
import EmailJob from '../../channels/email/EmailJob'
|
||||
import { RequestError } from '../../core/errors'
|
||||
import { addUserToList, createList } from '../../lists/ListService'
|
||||
import { createProject } from '../../projects/ProjectService'
|
||||
import { createTemplate } from '../../render/TemplateService'
|
||||
import { createSubscription } from '../../subscriptions/SubscriptionService'
|
||||
import { User } from '../../users/User'
|
||||
import { uuid } from '../../utilities'
|
||||
import Campaign from '../Campaign'
|
||||
import { allCampaigns, createCampaign, getCampaign, sendCampaign, sendList } from '../CampaignService'
|
||||
import * as CampaignService from '../CampaignService'
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('CampaignService', () => {
|
||||
|
||||
interface CampaignRefs {
|
||||
project_id: number
|
||||
template_id: number
|
||||
subscription_id: number
|
||||
}
|
||||
|
||||
const createCampaignDependencies = async (): Promise<CampaignRefs> => {
|
||||
const project = await createProject({ name: uuid() })
|
||||
const subscription = await createSubscription({
|
||||
project_id: project.id,
|
||||
name: uuid(),
|
||||
channel: 'email',
|
||||
})
|
||||
const template = await createTemplate({
|
||||
project_id: project.id,
|
||||
name: uuid(),
|
||||
type: 'email',
|
||||
data: {},
|
||||
})
|
||||
return {
|
||||
project_id: project.id,
|
||||
subscription_id: subscription.id,
|
||||
template_id: template.id,
|
||||
}
|
||||
}
|
||||
|
||||
const createTestCampaign = async (params?: CampaignRefs, extras?: Partial<Campaign>) => {
|
||||
params = params || await createCampaignDependencies()
|
||||
|
||||
const campaign = await createCampaign({
|
||||
name: uuid(),
|
||||
...params,
|
||||
...extras,
|
||||
})
|
||||
|
||||
return campaign
|
||||
}
|
||||
|
||||
const createUser = async (project_id: number): Promise<User> => {
|
||||
return await User.insertAndFetch({
|
||||
project_id,
|
||||
external_id: uuid(),
|
||||
data: {},
|
||||
})
|
||||
}
|
||||
|
||||
describe('allCampaigns', () => {
|
||||
test('return a list of campaigns', async () => {
|
||||
|
||||
const params = await createCampaignDependencies()
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await createTestCampaign(params)
|
||||
}
|
||||
|
||||
const campaigns = await allCampaigns(params.project_id)
|
||||
|
||||
expect(campaigns.length).toEqual(20)
|
||||
expect(campaigns[0].template_id).toEqual(params.template_id)
|
||||
expect(campaigns[0].subscription_id).toEqual(params.subscription_id)
|
||||
})
|
||||
|
||||
test('campaigns in other projects wont come back', async () => {
|
||||
|
||||
const params1 = await createCampaignDependencies()
|
||||
const params2 = await createCampaignDependencies()
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await createTestCampaign(params1)
|
||||
}
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await createTestCampaign(params2)
|
||||
}
|
||||
|
||||
const campaigns = await allCampaigns(params1.project_id)
|
||||
|
||||
expect(campaigns.length).toEqual(10)
|
||||
expect(campaigns[0].template_id).toEqual(params1.template_id)
|
||||
expect(campaigns[0].subscription_id).toEqual(params1.subscription_id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getCampaign', () => {
|
||||
test('return a single campaign', async () => {
|
||||
const response = await createTestCampaign()
|
||||
|
||||
const campaign = await getCampaign(response.id, response.project_id)
|
||||
|
||||
expect(campaign?.id).toEqual(response.id)
|
||||
})
|
||||
|
||||
test('a single campaign in a different project shouldnt return', async () => {
|
||||
const response = await createTestCampaign()
|
||||
const badParams = await createCampaignDependencies()
|
||||
|
||||
const campaign = await getCampaign(response.id, badParams.project_id)
|
||||
|
||||
expect(campaign?.id).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createCampaign', () => {
|
||||
test('create a single campaign', async () => {
|
||||
const params = await createCampaignDependencies()
|
||||
const name = uuid()
|
||||
const campaign = await createCampaign({
|
||||
...params,
|
||||
name,
|
||||
})
|
||||
|
||||
expect(campaign.name).toEqual(name)
|
||||
expect(campaign.subscription_id).toEqual(params.subscription_id)
|
||||
expect(campaign.project_id).toEqual(params.project_id)
|
||||
expect(campaign.template_id).toEqual(params.template_id)
|
||||
})
|
||||
|
||||
test('fail to create a campaign with a bad subscription', async () => {
|
||||
const params = await createCampaignDependencies()
|
||||
const name = uuid()
|
||||
const promise = createCampaign({
|
||||
project_id: params.project_id,
|
||||
template_id: params.template_id,
|
||||
subscription_id: 0,
|
||||
name,
|
||||
})
|
||||
await expect(promise).rejects.toThrowError(RequestError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendCampaign', () => {
|
||||
test('enqueue an email job', async () => {
|
||||
|
||||
const spy = jest.spyOn(App.main.queue, 'enqueue')
|
||||
|
||||
const campaign = await createTestCampaign()
|
||||
const user = await createUser(campaign.project_id)
|
||||
await sendCampaign(campaign, user)
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
expect(spy.mock.calls[0][0]).toBeInstanceOf(EmailJob)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendList', () => {
|
||||
test('enqueue sends for a list of people', async () => {
|
||||
const spy = jest.spyOn(CampaignService, 'sendCampaign')
|
||||
|
||||
const params = await createCampaignDependencies()
|
||||
const list = await createList({
|
||||
name: uuid(),
|
||||
project_id: params.project_id,
|
||||
rules: [],
|
||||
})
|
||||
const campaign = await createTestCampaign(params, {
|
||||
list_id: list.id,
|
||||
})
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const user = await createUser(params.project_id)
|
||||
await addUserToList(user, list)
|
||||
}
|
||||
|
||||
await sendList(campaign)
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(20)
|
||||
expect(spy.mock.calls[0][0].id).toEqual(campaign.id)
|
||||
})
|
||||
|
||||
test('users outside of list arent sent the campaign', async () => {
|
||||
const spy = jest.spyOn(CampaignService, 'sendCampaign')
|
||||
|
||||
const params = await createCampaignDependencies()
|
||||
const list = await createList({
|
||||
name: uuid(),
|
||||
project_id: params.project_id,
|
||||
rules: [],
|
||||
})
|
||||
const list2 = await createList({
|
||||
name: uuid(),
|
||||
project_id: params.project_id,
|
||||
rules: [],
|
||||
})
|
||||
const campaign = await createTestCampaign(params, {
|
||||
list_id: list.id,
|
||||
})
|
||||
|
||||
const inclusiveIds = []
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const user = await createUser(params.project_id)
|
||||
await addUserToList(user, list)
|
||||
inclusiveIds.push(user.id)
|
||||
}
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const user = await createUser(params.project_id)
|
||||
await addUserToList(user, list2)
|
||||
}
|
||||
|
||||
await sendList(campaign)
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(20)
|
||||
expect(spy.mock.calls[0][0].id).toEqual(campaign.id)
|
||||
expect(spy.mock.calls[0][1]).toEqual(inclusiveIds[0])
|
||||
})
|
||||
})
|
||||
})
|
|
@ -28,7 +28,7 @@ export default class JourneyDelayJob extends Job {
|
|||
for await (const chunk of stream) {
|
||||
|
||||
// TODO: Room for improvement here by not reprocessing
|
||||
// the entire queue but instead just this step (could
|
||||
// the entire journey but instead just this step (could
|
||||
// have some downsides through)
|
||||
App.main.queue.enqueue(
|
||||
JourneyProcessJob.from({
|
||||
|
|
|
@ -21,6 +21,14 @@ export const createList = async (params: ListParams): Promise<List> => {
|
|||
return await List.insertAndFetch(params)
|
||||
}
|
||||
|
||||
export const addUserToList = async (user: User, list: List, event?: UserEvent) => {
|
||||
return await UserList.insert({
|
||||
user_id: user.id,
|
||||
list_id: list.id,
|
||||
event_id: event?.id ?? undefined,
|
||||
})
|
||||
}
|
||||
|
||||
export const updateLists = async (user: User, event?: UserEvent) => {
|
||||
const lists = await List.all(qb => qb.where('project_id', user.project_id))
|
||||
const existingLists = await getUserListIds(user.id)
|
||||
|
@ -36,11 +44,7 @@ export const updateLists = async (user: User, event?: UserEvent) => {
|
|||
// If check passes and user isn't already in the list, add
|
||||
if (result && !existingLists.includes(list.id)) {
|
||||
|
||||
await UserList.insert({
|
||||
user_id: user.id,
|
||||
list_id: list.id,
|
||||
event_id: event?.id ?? undefined,
|
||||
})
|
||||
await addUserToList(user, list, event)
|
||||
|
||||
// Find all associated journeys based on list and enter user
|
||||
await enterJourneyFromList(list, user, event)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Model from '../core/Model'
|
||||
import Model, { ModelParams } from '../core/Model'
|
||||
|
||||
class Template extends Model {
|
||||
export class Template extends Model {
|
||||
project_id!: number
|
||||
name!: string
|
||||
type!: 'email' | 'text' | 'push_notification' | 'webhook'
|
||||
|
@ -9,6 +9,8 @@ class Template extends Model {
|
|||
static tableName = 'templates'
|
||||
}
|
||||
|
||||
export type TemplateParams = Omit<Template, ModelParams>
|
||||
|
||||
export class EmailTemplate extends Template {
|
||||
to!: string
|
||||
from!: string
|
||||
|
|
5
src/render/TemplateService.ts
Normal file
5
src/render/TemplateService.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { Template, TemplateParams } from './Template'
|
||||
|
||||
export const createTemplate = async (params: TemplateParams) => {
|
||||
return await Template.insertAndFetch(params)
|
||||
}
|
|
@ -6,6 +6,10 @@ export const random = <T>(array: T[]): T => array[Math.floor(Math.random() * arr
|
|||
|
||||
export const pascalToSnakeCase = (str: string): string => str.split(/(?=[A-Z])/).join('_').toLowerCase()
|
||||
|
||||
export const uuid = (): string => {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
|
||||
export const encrypt = (str: string): string => {
|
||||
const iv = crypto.randomBytes(16).toString('hex').slice(0, 16)
|
||||
const encrypter = crypto.createCipheriv('aes-256-cbc', process.env.APP_SECRET!, iv)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue