Files
speckle-server/packages/server/modules/activitystream/tests/integration/activity.graph.spec.ts
T
2025-08-28 10:02:53 +02:00

345 lines
12 KiB
TypeScript

/* istanbul ignore file */
import { expect } from 'chai'
import { beforeEachContext, initializeTestServer } from '@/test/hooks'
import { noErrors } from '@/test/helpers'
import { Roles, Scopes } from '@speckle/shared'
import { getUserActivityFactory } from '@/modules/activitystream/repositories'
import { db } from '@/db/knex'
import {
validateStreamAccessFactory,
addOrUpdateStreamCollaboratorFactory
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import {
getStreamRolesFactory,
grantStreamPermissionsFactory
} from '@/modules/core/repositories/streams'
import { getUserFactory } from '@/modules/core/repositories/users'
import { createPersonalAccessTokenFactory } from '@/modules/core/services/tokens'
import {
storePersonalApiTokenFactory,
storeApiTokenFactory,
storeTokenScopesFactory,
storeTokenResourceAccessDefinitionsFactory
} from '@/modules/core/repositories/tokens'
import { createObjectFactory } from '@/modules/core/services/objects/management'
import { storeSingleObjectIfNotFoundFactory } from '@/modules/core/repositories/objects'
import { getEventBus } from '@/modules/shared/services/eventBus'
import type http from 'node:http'
import type { BasicTestStream } from '@/test/speckle-helpers/streamHelper'
import { createTestStream } from '@/test/speckle-helpers/streamHelper'
import type { BasicTestBranch } from '@/test/speckle-helpers/branchHelper'
import { createTestBranch } from '@/test/speckle-helpers/branchHelper'
import { getActivitiesFactory } from '@/modules/activitystream/repositories/index'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
const getUser = getUserFactory({ db })
const getUserActivity = getUserActivityFactory({ db })
const validateStreamAccess = validateStreamAccessFactory({ authorizeResolver })
const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({
validateStreamAccess,
getUser,
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
getStreamRoles: getStreamRolesFactory({ db }),
emitEvent: getEventBus().emit
})
const createPersonalAccessToken = createPersonalAccessTokenFactory({
storeApiToken: storeApiTokenFactory({ db }),
storeTokenScopes: storeTokenScopesFactory({ db }),
storeTokenResourceAccessDefinitions: storeTokenResourceAccessDefinitionsFactory({
db
}),
storePersonalApiToken: storePersonalApiTokenFactory({ db })
})
const createObject = createObjectFactory({
storeSingleObjectIfNotFoundFactory: storeSingleObjectIfNotFoundFactory({ db })
})
const getActivities = getActivitiesFactory({ db })
let server: http.Server
let sendRequest: Awaited<ReturnType<typeof initializeTestServer>>['sendRequest']
describe('Activity @activity', () => {
let userIz: BasicTestUser
let userCr: BasicTestUser
let userX: BasicTestUser
let userIzToken: string
let userCrToken: string
let userXToken: string
const streamPublic: BasicTestStream = {
name: 'a fun stream for sharing',
description: 'for all to see!',
isPublic: true,
id: '',
ownerId: ''
}
// const collaboratorTestStream = {
// name: 'a fun stream for testing collab stuff',
// description: 'for all to see!',
// isPublic: true,
// id: undefined
// }
const branchPublic: BasicTestBranch = {
name: '🍁maple branch',
id: '',
streamId: '',
authorId: ''
}
const streamSecret: BasicTestStream = {
name: 'a secret stream for me',
description: 'for no one to see!',
isPublic: false,
id: '',
ownerId: ''
}
const testObj = {
hello: 'hallo',
cool: 'kult',
bunny: 'kanin',
id: ''
}
const testObj2 = {
goodbye: 'ha det bra',
warm: 'varmt',
bunny: 'kanin',
id: ''
}
before(async () => {
const ctx = await beforeEachContext()
server = ctx.server
;({ sendRequest } = await initializeTestServer(ctx))
const normalScopesList = [
Scopes.Streams.Read,
Scopes.Streams.Write,
Scopes.Users.Read,
Scopes.Users.Email,
Scopes.Tokens.Write,
Scopes.Tokens.Read,
Scopes.Profile.Read,
Scopes.Profile.Email
]
// create users
userIz = await createTestUser({
name: 'Izzy Lyseggen',
email: 'izzybizzi@speckle.systems',
password: 'sp0ckle sucks 9001'
})
userCr = await createTestUser({
name: 'Cristi Balas',
email: 'cristib@speckle.systems',
password: 'hack3r man 666'
})
userX = await createTestUser({
name: 'Mystery User',
email: 'mysteriousDude@speckle.systems',
password: 'super $ecret pw0rd'
})
userIzToken = `Bearer ${await createPersonalAccessToken(
userIz.id,
'izz test token',
normalScopesList
)}`
userCrToken = `Bearer ${await createPersonalAccessToken(
userCr.id,
'cristi test token',
normalScopesList
)}`
userXToken = `Bearer ${createPersonalAccessToken(
userX.id,
'no users:read test token',
[Scopes.Streams.Read, Scopes.Streams.Write]
)}`
// streams
// createStream({ ...collaboratorTestStream, ownerId: userIz.id }).then(
// (id) => (collaboratorTestStream.id = id)
// )
// It's definitely not great that there's a full on test case in the before() hook, but that's because
// these tests were originally written incorrectly - they depend on each other. So this is a temporary fix that
// ensures tests can be ran in any order
// + Avoiding GQL to bypass personal project limits, if any
// create stream (cr1)
await createTestStream(streamSecret, userCr)
// create commit (cr2)
testObj2.id = await createObject({ streamId: streamSecret.id, object: testObj2 })
const resCommit1 = await sendRequest(userCrToken, {
query: `mutation { commitCreate(commit: {streamId: "${streamSecret.id}", branchName: "main", objectId: "${testObj2.id}", message: "first commit"})}`
})
expect(noErrors(resCommit1))
// create stream #2 (iz1)
await createTestStream(streamPublic, userIz)
// create branch (iz2)
await createTestBranch({
branch: branchPublic,
stream: streamPublic,
owner: userIz
})
// create commit #2 (iz3)
testObj.id = await createObject({ streamId: streamPublic.id, object: testObj })
const resCommit2 = await sendRequest(userIzToken, {
query: `mutation { commitCreate(commit: { streamId: "${streamPublic.id}", branchName: "${branchPublic.name}", objectId: "${testObj.id}", message: "first commit" })}`
})
expect(noErrors(resCommit2))
// Add stream collaborator directly
await addOrUpdateStreamCollaborator(
streamPublic.id,
userCr.id,
Roles.Stream.Reviewer,
userIz.id
)
// update collaborator (iz4)
const resCollab = await sendRequest(userIzToken, {
query: `mutation { streamUpdatePermission( permissionParams: { streamId: "${streamPublic.id}", userId: "${userCr.id}", role: "stream:contributor" } ) }`
})
expect(noErrors(resCollab))
const { items: activityC } = await getUserActivity({ userId: userCr.id })
// 1: user created, 2: stream created, 3: commit created
expect(activityC.length).to.equal(3)
expect(activityC[0].actionType).to.equal('commit_create')
const activityI = await getUserActivity({ userId: userIz.id })
expect(activityI.items.length).to.equal(4)
expect(activityI).to.nested.include({
'items[0].actionType': 'commit_create',
'items[1].actionType': 'branch_create',
'items[2].actionType': 'stream_create',
'items[3].actionType': 'user_create'
})
const activity = { items: await getActivities({ userId: userIz.id }) }
expect(activity.items.length).to.equal(3)
expect(activity).to.nested.include({
'items[0].eventType': 'project_role_updated',
'items[0].payload.new': 'stream:owner',
'items[0].payload.old': null,
'items[0].userId': userIz.id, // created branch
'items[1].eventType': 'project_role_updated',
'items[1].payload.new': 'stream:reviewer',
'items[1].payload.old': null,
'items[1].payload.userId': userCr.id,
'items[1].userId': userIz.id, // added user
'items[2].eventType': 'project_role_updated',
'items[2].payload.new': 'stream:contributor',
'items[2].payload.old': 'stream:reviewer',
'items[2].payload.userId': userCr.id,
'items[2].userId': userIz.id // made him a contibutor
})
})
after(async () => {
await server.close()
})
it("Should get a user's own activity", async () => {
const res = await sendRequest(userIzToken, {
query: `query {activeUser { name activity { totalCount items {id streamId resourceType resourceId actionType userId message time}}} }`
})
expect(noErrors(res))
const activity = res.body.data.activeUser.activity
expect(activity.items.length).to.equal(4)
expect(activity.totalCount).to.equal(4)
expect(activity.items[0].actionType).to.equal('commit_create')
expect(activity.items[activity.totalCount - 1].actionType).to.equal('user_create')
})
it("Should get another user's activity", async () => {
const res = await sendRequest(userIzToken, {
query: `query {otherUser(id:"${userCr.id}") { name activity { totalCount items {id streamId resourceType resourceId actionType userId message time}}} }`
})
expect(noErrors(res))
expect(res.body.data.otherUser.activity.items.length).to.equal(3)
expect(res.body.data.otherUser.activity.totalCount).to.equal(3)
})
it("Should get a user's timeline", async () => {
const res = await sendRequest(userIzToken, {
query: `query {otherUser(id:"${userCr.id}") { name timeline { totalCount items {id streamId resourceType resourceId actionType userId message time}}} }`
})
expect(noErrors(res))
expect(res.body.data.otherUser.timeline.items.length).to.equal(5) // sum of all actions in before hook
expect(res.body.data.otherUser.timeline.totalCount).to.equal(5)
})
it("Should get a stream's activity", async () => {
const res = await sendRequest(userCrToken, {
query: `query { stream(id: "${streamPublic.id}") { activity { totalCount items {id streamId resourceId actionType message} } } }`
})
expect(noErrors(res))
const activity = res.body.data.stream.activity
expect(activity.items.length).to.equal(3)
expect(activity.totalCount).to.equal(3)
expect(activity.items[activity.totalCount - 1].actionType).to.equal('stream_create')
})
it("Should get a branch's activity", async () => {
const res = await sendRequest(userCrToken, {
query: `query { stream(id: "${streamPublic.id}") { branch(name: "${branchPublic.name}") { activity { totalCount items {id streamId resourceId actionType message} } } } }`
})
expect(noErrors(res))
const activity = res.body.data.stream.branch.activity
expect(activity.items.length).to.equal(1)
expect(activity.totalCount).to.equal(1)
expect(activity.items[0].actionType).to.equal('branch_create')
})
it("Should *not* get a stream's activity if you don't have access to it", async () => {
const res = await sendRequest(userIzToken, {
query: `query {stream(id:"${streamSecret.id}") {name activity {items {id streamId resourceType resourceId actionType userId message time}}} }`
})
expect(res.body.errors?.length).to.equal(1)
})
it("Should *not* get a stream's activity if you are not a server user", async () => {
const res = await sendRequest(null, {
query: `query {stream(id:"${streamPublic.id}") {name activity {items {id streamId resourceType resourceId actionType userId message time}}} }`
})
expect(res.body.errors?.length).to.equal(1)
})
it("Should *not* get a user's activity without the `users:read` scope", async () => {
const res = await sendRequest(userXToken, {
query: `query {otherUser(id:"${userCr.id}") { name activity {items {id streamId resourceType resourceId actionType userId message time}}} }`
})
expect(res.body.error).to.exist
})
it("Should *not* get a user's timeline without the `users:read` scope", async () => {
const res = await sendRequest(userXToken, {
query: `query {otherUser(id:"${userCr.id}") { name timeline {items {id streamId resourceType resourceId actionType userId message time}}} }`
})
expect(res.body.error).to.exist
})
})