Files
speckle-server/packages/server/modules/activitystream/tests/integration/activitySummary.spec.ts
T
Daniel Gak Anagrov 399c998fd7 feat(multiregion): apply prepared transactions to projects (#5322)
* feat(multiregion): replace user replication

* chore(multiregion): optimise replication

* maybe it's this

* postgres is fun

* once more

* chore(multiregion): only replicate test user creation during multiregion tests

* feat: improved replicate_query logic

* fix: minor

* fix: starting issue

* feat: included user create and delete specs to multiregion

* feat: removed console logs

* fix: user defaults

* fix: multiregion test helper

* fix: update scenarios for users

* refactor(multiregion): swap replicateQuery concept to asMultiregionOperation (#5301)

feat(multiregion): introduced asMultregionOperator, refactor test to user builder classes

* chore: renamings

* fix: remove comments

* feat: remove user replication

* refactor: simplified spec usages

* chore: comments

* chore: branches and favs

* chore: more tests

* chore: more tests

* fix linting

* fix tests

* feat: dropping replication

* refactor: moved project delete to service

* fix: comment

* feat: updateStreamFactory and updateProjectFacotry

* deleteProjectFactory + replicateFactory

* deleteWorkspaceFactory

* fix: selector

* fix: tests

* fix tests, finished createStreamFactory

* feat: simplify changes

* fix: remove comment

* fix: minor strucutres

* fix: moveProjectToRegion

* fix: moved branch creation outside of multiregion scope

* fix: branch creation

* fix: tests

* fix: ci tests

* fix: removed log form test

* fix: on specs, no random regionKeys

* review fixes

* fix: mr comments

* feat: removed test

---------

Co-authored-by: Charles Driesler <chuck@speckle.systems>
2025-09-04 13:07:19 +02:00

204 lines
6.7 KiB
TypeScript

import { truncateTables } from '@/test/hooks'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUsers } from '@/test/authHelper'
import { StreamActivity, Users } from '@/modules/core/dbSchema'
import {
createActivitySummaryFactory,
sendActivityNotificationsFactory
} from '@/modules/activitystream/services/summary'
import { expect } from 'chai'
import {
StreamActionTypes,
StreamResourceTypes
} from '@/modules/activitystream/helpers/types'
import type {
ActivityDigestMessage,
NotificationType,
NotificationTypeMessageMap
} from '@/modules/notifications/helpers/types'
import {
geUserStreamActivityFactory,
saveStreamActivityFactory
} from '@/modules/activitystream/repositories'
import { db } from '@/db/knex'
import { getStreamFactory } from '@/modules/core/repositories/streams'
import { getUserFactory } from '@/modules/core/repositories/users'
import { createTestStream } from '@/test/speckle-helpers/streamHelper'
import { deleteProjectAndCommitsFactory } from '@/modules/core/services/projects'
import { deleteProjectFactory } from '@/modules/core/repositories/projects'
import { deleteProjectCommitsFactory } from '@/modules/core/repositories/commits'
import type { DeleteProjectAndCommits } from '@/modules/core/domain/projects/operations'
import { asMultiregionalOperation, replicateFactory } from '@/modules/shared/command'
import { logger } from '@/observability/logging'
import { getProjectReplicationDbs } from '@/modules/multiregion/utils/dbSelector'
const cleanup = async () => {
await truncateTables([StreamActivity.name, Users.name])
}
const getUser = getUserFactory({ db })
const getStream = getStreamFactory({ db })
const saveActivity = saveStreamActivityFactory({ db })
const createActivitySummary = createActivitySummaryFactory({
getStream,
getActivity: geUserStreamActivityFactory({ db }),
getUser
})
const deleteStreamAndCommits: DeleteProjectAndCommits = async ({ projectId }) =>
asMultiregionalOperation(
async ({ allDbs }) =>
// this is a bit of an overhead, we are issuing delete queries to all regions,
// instead of being selective and clever about figuring out the project DB and only
// deleting from main and the project db
deleteProjectAndCommitsFactory({
deleteProject: replicateFactory(allDbs, deleteProjectFactory),
deleteProjectCommits: replicateFactory(allDbs, deleteProjectCommitsFactory)
})({ projectId }),
{
name: 'deleteStreamAndCommits spec',
logger,
dbs: await getProjectReplicationDbs({ projectId })
}
)
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({
userId: 'notAUserId',
streamIds: ['someStreamIds'],
start: new Date(),
end: 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) => (await createTestStream(stream, userA)).id
)
)
const summary = await createActivitySummary({
userId: userA.id,
streamIds,
start,
end: new Date()
})
// stream creation is an activity
expect(summary?.streamActivities).to.have.length(2)
})
it('gets activities for the user', async () => {
const start = new Date()
const streamIds = await Promise.all(
[{ name: 'foo' }, { name: 'bar' }].map(
async (stream) => (await createTestStream(stream, userA)).id
)
)
const summary = await createActivitySummary({
userId: userA.id,
streamIds,
start,
end: new Date()
})
expect(summary?.streamActivities).to.have.length(2)
})
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) => (await createTestStream(stream, userA)).id
)
)
await saveActivity({
streamId,
resourceType: StreamResourceTypes.Stream,
resourceId: streamId,
actionType: StreamActionTypes.Stream.Create,
userId: userA.id,
info: {},
message: 'foo'
})
await deleteStreamAndCommits({ projectId: streamId })
const summary = await createActivitySummary({
userId: userA.id,
streamIds: [streamId],
start,
end: 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 sendActivityNotificationsFactory({
publishNotification: notificationPublisher.publish.bind(notificationPublisher),
getActiveUserStreams: async () => []
})(new Date(), new Date())
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 sendActivityNotificationsFactory({
publishNotification: notificationPublisher.publish.bind(notificationPublisher),
getActiveUserStreams: async () => userStreams
})(new Date(), new Date())
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)
})
})
})