Files
speckle-server/packages/server/modules/core/tests/batchCommits.spec.ts
T
Daniel Gak Anagrov 55f91d2cdf fix(gql): scopes, roles, auth (#5724)
* fix(workspace): auto approval
* fix(scopes): access scopes across the server
* fix(hasAccessRole): establish for all mutations
* feat(token): scoping does not require the token to exist
* chore(scopes): added additional roles
* fix: replaced UNAUTHORIZED_ACCESS_ERROR with UNAUTHORIZED
* fix(email): user list scopes
2025-10-29 09:53:11 +00:00

331 lines
9.8 KiB
TypeScript

import { buildApolloServer } from '@/app'
import { db } from '@/db/knex'
import { Commits, Streams, Users } from '@/modules/core/dbSchema'
import { Roles } from '@/modules/core/helpers/mainConstants'
import { createBranchFactory } from '@/modules/core/repositories/branches'
import { getCommitsFactory } from '@/modules/core/repositories/commits'
import {
getStreamRolesFactory,
grantStreamPermissionsFactory
} from '@/modules/core/repositories/streams'
import { getUserFactory } from '@/modules/core/repositories/users'
import {
addOrUpdateStreamCollaboratorFactory,
validateStreamAccessFactory
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import { getEventBus } from '@/modules/shared/services/eventBus'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUsers } from '@/test/authHelper'
import { deleteCommits, moveCommits } from '@/test/graphql/commits'
import type { ServerAndContext } from '@/test/graphqlHelper'
import { createAuthedTestContext, createTestContext } from '@/test/graphqlHelper'
import { truncateTables } from '@/test/hooks'
import type { BasicTestCommit } from '@/test/speckle-helpers/commitHelper'
import { createTestCommits } from '@/test/speckle-helpers/commitHelper'
import type { BasicTestStream } from '@/test/speckle-helpers/streamHelper'
import { createTestStreams } from '@/test/speckle-helpers/streamHelper'
import { expect } from 'chai'
import { times } from 'lodash-es'
enum BatchActionType {
Move,
Delete
}
const getUser = getUserFactory({ db })
const createBranch = createBranchFactory({ db })
const getCommits = getCommitsFactory({ db })
const validateStreamAccess = validateStreamAccessFactory({ authorizeResolver })
const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({
validateStreamAccess,
getUser,
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
getStreamRoles: getStreamRolesFactory({ db }),
emitEvent: getEventBus().emit
})
const cleanup = async () => {
await truncateTables([Streams.name, Users.name, Commits.name])
}
describe('Batch commits', () => {
const userCommmitCount = 10
const secondBranchName = 'second'
const me: BasicTestUser = {
name: 'batch commit dude',
email: 'batchcommitguy@example.org',
id: ''
}
const otherGuy: BasicTestUser = {
name: 'other batch commit guy',
email: 'otherbatchcommitguy@example.org',
id: ''
}
const myStream: BasicTestStream = {
name: 'my first test stream',
isPublic: false,
ownerId: '',
id: ''
}
const otherStream: BasicTestStream = {
name: 'other guys first test stream',
isPublic: false,
ownerId: '',
id: ''
}
let myCommits: BasicTestCommit[]
let otherCommits: BasicTestCommit[]
let streamId: string
before(async () => {
await cleanup()
await createTestUsers([me, otherGuy])
await createTestStreams([
[myStream, me],
[otherStream, otherGuy]
])
await Promise.all([
// create another branch for each stream
createBranch({
name: secondBranchName,
description: '',
streamId: myStream.id,
authorId: me.id
}),
createBranch({
name: secondBranchName,
description: '',
streamId: otherStream.id,
authorId: otherGuy.id
}),
// add users as contributors to each others streams
addOrUpdateStreamCollaborator(
otherStream.id,
me.id,
Roles.Stream.Contributor,
otherGuy.id
)
])
myCommits = times(userCommmitCount, (i): BasicTestCommit => {
streamId = i % 2 === 0 ? myStream.id : otherStream.id
return {
id: '',
objectId: '',
branchId: '',
streamId,
authorId: me.id
}
})
otherCommits = times(
userCommmitCount,
(): BasicTestCommit => ({
id: '',
objectId: '',
branchId: '',
streamId: otherStream.id,
authorId: otherGuy.id
})
)
await createTestCommits([...myCommits, ...otherCommits])
})
const batchActionDataSet = [
{ display: 'move', type: BatchActionType.Move },
{ display: 'delete', type: BatchActionType.Delete }
]
const buildBatchActionInvoker =
(apollo: ServerAndContext) => (type: BatchActionType, commitIds: string[]) => {
if (type === BatchActionType.Delete) {
return deleteCommits(apollo, { input: { commitIds, streamId } })
} else if (type === BatchActionType.Move) {
return moveCommits(apollo, {
input: { commitIds, targetBranch: secondBranchName, streamId }
})
} else {
throw new Error('Unexpected batch action type')
}
}
type BatchActionInvoker = ReturnType<typeof buildBatchActionInvoker>
describe('when authenticated', () => {
let apollo: ServerAndContext
let invokeBatchAction: BatchActionInvoker
before(async () => {
apollo = {
apollo: await buildApolloServer(),
context: await createAuthedTestContext(me.id)
}
invokeBatchAction = buildBatchActionInvoker(apollo)
})
batchActionDataSet.forEach(({ display, type }) => {
it(`can't batch ${display} commits if not commit or stream author`, async () => {
const result = await invokeBatchAction(
type,
otherCommits.map((c) => c.id)
)
expect(result).to.haveGraphQLErrors({
code: 'FORBIDDEN'
})
})
it(`can't batch ${display} an empty commit array`, async () => {
const result = await invokeBatchAction(type, [])
expect(result).to.haveGraphQLErrors('No commits specified')
})
it(`can't batch ${display} commits if at least one is nonexistant`, async () => {
const result = await invokeBatchAction(type, [
...myCommits.map((c) => c.id),
'aaaaaaaa'
])
expect(result).to.haveGraphQLErrors({
code: 'NOT_FOUND_ERROR'
})
})
})
describe('and deleting commits', async () => {
const deletableCommitCount = 5
let myDeletableCommits: BasicTestCommit[]
beforeEach(async () => {
myDeletableCommits = times(deletableCommitCount, (i): BasicTestCommit => {
streamId = i % 2 === 0 ? myStream.id : otherStream.id
return {
id: '',
branchId: '',
streamId,
objectId: '',
authorId: me.id
}
})
await createTestCommits(myDeletableCommits)
})
const invokeDelete = (commitIds: string[]) =>
deleteCommits(apollo, { input: { commitIds, streamId } })
const validateDeleted = async (commitIds: string[]) => {
const commits = await getCommits(commitIds)
expect(commits).to.be.empty
}
it('can do it for commits of multiple streams', async () => {
const commitIds = myDeletableCommits.map((c) => c.id)
const result = await invokeDelete(commitIds)
expect(result).to.not.haveGraphQLErrors()
await validateDeleted(commitIds)
})
})
describe('and moving commits', async () => {
const movableCommitCount = 5
let myMovableCommits: BasicTestCommit[]
let streamId: string
before(async () => {
myMovableCommits = times(movableCommitCount, (i): BasicTestCommit => {
streamId = i % 2 === 0 ? myStream.id : otherStream.id
return {
id: '',
branchId: '',
objectId: '',
streamId,
authorId: me.id
}
})
await createTestCommits(myMovableCommits)
})
const invokeMove = (commitIds: string[], targetBranch = secondBranchName) =>
moveCommits(apollo, { input: { commitIds, targetBranch, streamId } })
const validateMoved = async (
commitIds: string[],
targetBranch = secondBranchName
) => {
const commits = await getCommits(commitIds)
const areAllMoved =
commits.length === commitIds.length &&
commits.every((c) => c.branchName === targetBranch)
expect(areAllMoved).to.be.true
}
it("can't do it for commits belonging to multiple streams", async () => {
const commitIds = myMovableCommits.map((c) => c.id)
const result = await invokeMove(commitIds)
expect(result).to.haveGraphQLErrors('commits belong to different streams')
})
it('moves to new branch when specifying a nonexistant target branch', async () => {
const newBranchName = 'some-nonexistant-stream'
const commitIds = myMovableCommits
.filter((c) => c.streamId === myStream.id)
.map((c) => c.id)
const result = await invokeMove(commitIds, newBranchName)
expect(result).to.not.haveGraphQLErrors()
await validateMoved(commitIds, newBranchName)
})
it('can do it with commits belonging to the same stream', async () => {
const commitIds = myMovableCommits
.filter((c) => c.streamId === myStream.id)
.map((c) => c.id)
const result = await invokeMove(commitIds)
expect(result).to.not.haveGraphQLErrors()
await validateMoved(commitIds)
})
})
})
describe('when not authenticated', () => {
let apollo: ServerAndContext
let invokeBatchAction: BatchActionInvoker
before(async () => {
apollo = {
apollo: await buildApolloServer(),
context: await createTestContext()
}
invokeBatchAction = buildBatchActionInvoker(apollo)
})
batchActionDataSet.forEach(({ display, type }) => {
it(`can't batch ${display} commits`, async () => {
const result = await invokeBatchAction(
type,
myCommits.map((c) => c.id)
)
expect(result).to.haveGraphQLErrors('Must provide an auth token')
})
})
})
})