d1d5984e30
* refactor(server emails): email transports module refactor to TypeScript * refactor(docker-compose deps): move local email server to common dev compose file * chore(server launch.json): add ts-node script running example * chore(server deps): add nodemailer types package * refactor(server activitystream): add strongly typed activity definitions * feat(server activitystream): add activity repository * feat(server info): add canonical url on the service level * feat(server): add static file serving route to server core * feat(server): add dependencies for periodical email digests * feat(server activity stream): call the initialization step from the activity stream module * feat(server activity digest): add WIP weekly email digest implementation * feat(server digest email): smul upgrades and fixes to the email template and its contents * just for Fabs to test * chore(root package.json): remove deleted docker-compose references * feat(frontend profile): add notification preferences panel * feat(server digest emails): set prod ready cron tab and timespan * refactor(server email digest): move templates into the email module * refactor(server activity digests): refactor to use notifications infrastructure * test(server activities): add tests and some refactor to activities and notification preferences * refactor(notification preferences): fix minor issues * test(server notification preferences test): fix describe nesting * fix(server activities): add missing action types * fix(server activities): fix errors after merging main * test(server activity notifications): add test coverage for activity notifications service * refactor(server activities): fixing tests and some cleanup * feat(server cli): add summary notification command to cli * chore(dev env db versions): upgrade local dev env versions * chore(server deps): upgrade local dev db to pg 14 * fix(docker-compose): bind maildev to localhost * process-scoped notifications test queues * test(activity tests): add sleep to fix flaky CI * feat(activity digests): add demo date for digest trigger * feat(activity digest): add UK timezone trigger date Co-authored-by: Iain Sproat <68657+iainsproat@users.noreply.github.com> Co-authored-by: Fabians <fabis94@live.com>
175 lines
5.0 KiB
TypeScript
175 lines
5.0 KiB
TypeScript
import { truncateTables } from '@/test/hooks'
|
|
import { BasicTestUser, createTestUsers } from '@/test/authHelper'
|
|
import { StreamActivity, Users } from '@/modules/core/dbSchema'
|
|
import {
|
|
createActivitySummary,
|
|
sendActivityNotifications
|
|
} from '@/modules/activitystream/services/summary'
|
|
import { expect } from 'chai'
|
|
import { createStream, deleteStream } from '@/modules/core/services/streams'
|
|
import { saveActivity } from '@/modules/activitystream/services'
|
|
import { ActionTypes, ResourceTypes } from '@/modules/activitystream/helpers/types'
|
|
import {
|
|
ActivityDigestMessage,
|
|
NotificationType,
|
|
NotificationTypeMessageMap
|
|
} from '@/modules/notifications/helpers/types'
|
|
import { sleep } from '@/test/helpers'
|
|
|
|
const cleanup = async () => {
|
|
await truncateTables([StreamActivity.name, Users.name])
|
|
}
|
|
|
|
describe('Activity summary @activity', () => {
|
|
const userA: BasicTestUser = {
|
|
name: 'd1',
|
|
email: 'd.1@speckle.systems',
|
|
id: ''
|
|
}
|
|
before(async () => {
|
|
await cleanup()
|
|
await createTestUsers([userA])
|
|
})
|
|
describe('create activity summary', () => {
|
|
it('returns null for non existing users', async () => {
|
|
const summary = await createActivitySummary(
|
|
'notAUserId',
|
|
['someStreamIds'],
|
|
new Date(),
|
|
new Date()
|
|
)
|
|
expect(summary).to.be.null
|
|
})
|
|
it('no activity returns empty summary', async () => {
|
|
const start = new Date()
|
|
const streamIds = await Promise.all(
|
|
[{ name: 'foo' }, { name: 'bar' }].map(async (stream) =>
|
|
createStream({ ...stream, ownerId: userA.id })
|
|
)
|
|
)
|
|
|
|
const summary = await createActivitySummary(
|
|
userA.id,
|
|
streamIds,
|
|
start,
|
|
new Date()
|
|
)
|
|
|
|
expect(summary?.streamActivities).to.have.length(0)
|
|
})
|
|
it('gets activities for the user', async () => {
|
|
const start = new Date()
|
|
const streamIds = await Promise.all(
|
|
[{ name: 'foo' }, { name: 'bar' }].map(async (stream) =>
|
|
createStream({ ...stream, ownerId: userA.id })
|
|
)
|
|
)
|
|
await saveActivity({
|
|
streamId: streamIds[0],
|
|
resourceType: ResourceTypes.Stream,
|
|
resourceId: streamIds[0],
|
|
actionType: ActionTypes.Stream.Create,
|
|
userId: userA.id,
|
|
info: {},
|
|
message: 'foo'
|
|
})
|
|
await sleep(100)
|
|
const summary = await createActivitySummary(
|
|
userA.id,
|
|
streamIds,
|
|
start,
|
|
new Date()
|
|
)
|
|
|
|
expect(summary?.streamActivities).to.have.length(1)
|
|
})
|
|
|
|
it('if stream is deleted, activity summary returns with null as stream value', async () => {
|
|
const start = new Date()
|
|
const [streamId] = await Promise.all(
|
|
[{ name: 'foo' }].map(async (stream) =>
|
|
createStream({ ...stream, ownerId: userA.id })
|
|
)
|
|
)
|
|
await saveActivity({
|
|
streamId,
|
|
resourceType: ResourceTypes.Stream,
|
|
resourceId: streamId,
|
|
actionType: ActionTypes.Stream.Create,
|
|
userId: userA.id,
|
|
info: {},
|
|
message: 'foo'
|
|
})
|
|
await deleteStream({ streamId })
|
|
const summary = await createActivitySummary(
|
|
userA.id,
|
|
[streamId],
|
|
start,
|
|
new Date()
|
|
)
|
|
|
|
expect(summary?.streamActivities).to.have.length(1)
|
|
expect(summary?.streamActivities[0].stream).to.be.null
|
|
})
|
|
})
|
|
|
|
type NotificationMessage<T extends NotificationType> = {
|
|
type: T
|
|
params: Omit<NotificationTypeMessageMap[T], 'type'>
|
|
}
|
|
|
|
class FakeNotificationPublisher {
|
|
notifications: NotificationMessage<NotificationType>[] = []
|
|
|
|
async publish<T extends NotificationType>(
|
|
type: T,
|
|
params: Omit<NotificationTypeMessageMap[T], 'type'>
|
|
): Promise<string | number> {
|
|
this.notifications.push({ type, params })
|
|
return this.notifications.length
|
|
}
|
|
|
|
constructor() {
|
|
this.notifications = []
|
|
}
|
|
}
|
|
|
|
describe('send activity notifications', () => {
|
|
it('sends no notifications if there are no active streams', async () => {
|
|
const notificationPublisher = new FakeNotificationPublisher()
|
|
await sendActivityNotifications(
|
|
new Date(),
|
|
new Date(),
|
|
notificationPublisher.publish.bind(notificationPublisher),
|
|
async () => []
|
|
)
|
|
|
|
expect(notificationPublisher.notifications.length).to.equal(0)
|
|
})
|
|
|
|
it('for each UserStream a notification is sent', async () => {
|
|
const userStreams = [
|
|
{ userId: 'foo', streamIds: ['bar'] },
|
|
{ userId: 'boo', streamIds: ['tic', 'tac', 'toe'] }
|
|
]
|
|
const notificationPublisher = new FakeNotificationPublisher()
|
|
await sendActivityNotifications(
|
|
new Date(),
|
|
new Date(),
|
|
notificationPublisher.publish.bind(notificationPublisher),
|
|
async () => userStreams
|
|
)
|
|
|
|
expect(
|
|
notificationPublisher.notifications
|
|
.map((n) => n.params)
|
|
.filter((p): p is ActivityDigestMessage => true)
|
|
.map((n) => ({
|
|
userId: n.targetUserId,
|
|
streamIds: n.data.streamIds
|
|
}))
|
|
).to.be.deep.equalInAnyOrder(userStreams)
|
|
})
|
|
})
|
|
})
|