Files
speckle-server/packages/server/modules/core/tests/streams.spec.ts
T
2023-07-28 11:57:28 +02:00

580 lines
17 KiB
TypeScript

import { expect } from 'chai'
import {
createStream,
getStream,
updateStream,
deleteStream,
getStreamUsers,
grantPermissionsStream
} from '../services/streams'
import {
createBranch,
getBranchByNameAndStreamId,
deleteBranchById
} from '../services/branches'
import { createObject } from '../services/objects'
import { createCommitByBranchName } from '../services/commits'
import { beforeEachContext, truncateTables } from '@/test/hooks'
import {
addOrUpdateStreamCollaborator,
isStreamCollaborator
} from '@/modules/core/services/streams/streamAccessService'
import { Roles } from '@/modules/core/helpers/mainConstants'
import {
buildAuthenticatedApolloServer,
buildUnauthenticatedApolloServer
} from '@/test/serverHelper'
import {
getLimitedUserStreams,
getUserStreams,
leaveStream
} from '@/test/graphql/streams'
import { BasicTestUser, createTestUsers } from '@/test/authHelper'
import {
BasicTestStream,
createTestStream,
createTestStreams
} from '@/test/speckle-helpers/streamHelper'
import {
StreamWithOptionalRole,
revokeStreamPermissions
} from '@/modules/core/repositories/streams'
import { has, times } from 'lodash'
import { Streams } from '@/modules/core/dbSchema'
import { ApolloServer } from 'apollo-server-express'
import { Nullable } from '@/modules/shared/helpers/typeHelper'
import { sleep } from '@/test/helpers'
import dayjs, { Dayjs } from 'dayjs'
import {
GetLimitedUserStreamsQuery,
GetUserStreamsQuery
} from '@/test/graphql/generated/graphql'
import { Get } from 'type-fest'
import { changeUserRole } from '@/modules/core/services/users'
describe('Streams @core-streams', () => {
const userOne: BasicTestUser = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie@gmail.com',
password: 'sn3aky-1337-b1m',
id: ''
}
const userTwo: BasicTestUser = {
name: 'Dimitrie Stefanescu 2',
email: 'didimitrie2@gmail.com',
password: 'sn3aky-1337-b1m',
id: ''
}
const testStream: BasicTestStream = {
name: 'Test Stream 01',
description: 'wonderful test stream',
isPublic: true,
ownerId: '',
id: ''
}
const secondTestStream: BasicTestStream = {
name: 'Test Stream 02',
description: 'wot',
isPublic: false,
ownerId: '',
id: ''
}
const userLimitedUserDataSet = [
{ display: 'User', limitedUser: false },
{ display: 'LimitedUser', limitedUser: true }
]
before(async () => {
await beforeEachContext()
await createTestUsers([userOne, userTwo])
await createTestStreams([
[testStream, userOne],
[secondTestStream, userOne]
])
})
describe('Create, Read, Update, Delete Streams', () => {
it('Should create a stream', async () => {
const stream1Id = await createStream({ ...testStream, ownerId: userOne.id })
expect(stream1Id).to.not.be.null
const stream2Id = await createStream({
...secondTestStream,
ownerId: userOne.id
})
expect(stream2Id).to.not.be.null
})
it('Should get a stream', async () => {
const stream = await getStream({ streamId: testStream.id })
expect(stream).to.not.be.null
})
it('Should update a stream', async () => {
await updateStream({
id: testStream.id,
name: 'Modified Name',
description: 'Wooot'
})
const stream = await getStream({ streamId: testStream.id })
expect(stream?.name).to.equal('Modified Name')
expect(stream?.description).to.equal('Wooot')
})
// it('Should get all streams of a user', async () => {
// const { streams, cursor } = await getUserStreams({ userId: userOne.id })
// expect(streams).to.be.ok
// expect(cursor).to.be.ok
// expect(streams).to.not.be.empty
// })
// it('Should search all streams of a user', async () => {
// const { streams, cursor } = await getUserStreams({
// userId: userOne.id,
// searchQuery: 'woo'
// })
// // console.log( res )
// expect(streams).to.have.lengthOf(1)
// expect(cursor).to.exist
// })
it('Should delete a stream', async () => {
const id = await createStream({
name: 'mayfly',
description: 'wonderful',
ownerId: userOne.id
})
await deleteStream({ streamId: id })
const stream = await getStream({ streamId: id })
expect(stream).to.not.be.ok
})
})
describe('Sharing: Grant & Revoke permissions', () => {
before(async () => {
await addOrUpdateStreamCollaborator(
testStream.id,
userTwo.id,
Roles.Stream.Contributor,
userOne.id
)
})
it('Should get the users with access to a stream', async () => {
const users = await getStreamUsers({ streamId: testStream.id })
expect(users).to.have.lengthOf(2)
expect(users[0]).to.not.have.property('email')
expect(users[0]).to.have.property('id')
})
it('Should revoke permissions on stream', async () => {
await revokeStreamPermissions({ streamId: testStream.id, userId: userTwo.id })
const streamWithRole = await getStream({
streamId: testStream.id,
userId: userTwo.id
})
expect(streamWithRole?.role).to.be.not.ok
})
it('Should not revoke owner permissions', async () => {
await revokeStreamPermissions({ streamId: testStream.id, userId: userOne.id })
.then(() => {
throw new Error('This should have thrown')
})
.catch((err) => {
expect(err.message).to.include('cannot revoke permissions.')
})
})
it('Collaborator can leave a stream on his own', async () => {
const streamId = await createStream({
name: 'test streammmmm',
description: 'ayy',
isPublic: false,
ownerId: userOne.id
})
await addOrUpdateStreamCollaborator(
streamId,
userTwo.id,
Roles.Stream.Reviewer,
userOne.id
)
const apollo = await buildAuthenticatedApolloServer(userTwo.id)
const { data, errors } = await leaveStream(apollo, { streamId })
expect(errors).to.be.not.ok
expect(data?.streamLeave).to.be.ok
const userIsCollaborator = await isStreamCollaborator(userTwo.id, streamId)
expect(userIsCollaborator).to.not.be.ok
})
it('Server guests cannot be stream owners', async () => {
const guestGuy: BasicTestUser = {
name: 'Some we do not fully trust',
email: 'shady@contractor.company',
password: 'foobar123',
id: ''
}
await createTestUsers([guestGuy])
await changeUserRole({
userId: guestGuy.id,
role: Roles.Server.Guest,
guestModeEnabled: true
})
await addOrUpdateStreamCollaborator(
testStream.id,
guestGuy.id,
Roles.Stream.Owner,
userOne.id
)
.then(() => {
throw new Error('This should have thrown')
})
.catch((err) => {
expect(err.message).to.include('Server guests cannot own streams')
})
})
})
describe('`UpdatedAt` prop update', () => {
let updatableStream: StreamWithOptionalRole
before(async () => {
const id = await createStream({
name: 'T1',
ownerId: userOne.id,
isPublic: false
})
const newStream = await getStream({ streamId: id })
if (!newStream) throw new Error("Couldn't create stream")
updatableStream = newStream
})
afterEach(async () => {
// refresh updatedAt
const stream = await getStream({ streamId: updatableStream.id })
if (!stream) throw new Error("Couldn't create stream")
updatableStream = stream
})
it('Should update stream updatedAt on stream update ', async () => {
await updateStream({ id: updatableStream.id, name: 'TU1' })
const su = await getStream({ streamId: updatableStream.id })
expect(su?.updatedAt).to.be.ok
expect(su!.updatedAt).to.not.equal(updatableStream.updatedAt)
})
it('Should update stream updatedAt on sharing operations ', async () => {
let lastUpdatedAt = updatableStream.updatedAt
await grantPermissionsStream({
streamId: updatableStream.id,
userId: userTwo.id,
role: Roles.Stream.Contributor
})
// await sleep(100)
let su = await getStream({ streamId: updatableStream.id })
expect(su?.updatedAt).to.be.ok
expect(su!.updatedAt).to.not.equal(lastUpdatedAt)
lastUpdatedAt = su!.updatedAt
await revokeStreamPermissions({
streamId: updatableStream.id,
userId: userTwo.id
})
// await sleep(100)
su = await getStream({ streamId: updatableStream.id })
expect(su?.updatedAt).to.be.ok
expect(su!.updatedAt).to.not.equal(lastUpdatedAt)
})
it('Should update stream updatedAt on branch operations ', async () => {
let lastUpdatedAt = updatableStream.updatedAt
await createBranch({
name: 'dim/lol',
streamId: updatableStream.id,
authorId: userOne.id,
description: 'ayyyy'
})
const su = await getStream({ streamId: updatableStream.id })
expect(su?.updatedAt).to.be.ok
expect(su!.updatedAt).to.not.equal(lastUpdatedAt)
lastUpdatedAt = su!.updatedAt
// await sleep(100)
const b = await getBranchByNameAndStreamId({
streamId: updatableStream.id,
name: 'dim/lol'
})
await deleteBranchById({
id: b!.id,
streamId: updatableStream.id,
userId: userOne.id
})
const su2 = await getStream({ streamId: updatableStream.id })
expect(su2?.updatedAt).to.be.ok
expect(su2!.updatedAt).to.not.equal(lastUpdatedAt)
})
it('Should update stream updatedAt on commit operations ', async () => {
const testObject = { foo: 'bar', baz: 'qux', id: '' }
testObject.id = await createObject(updatableStream.id, testObject)
await createCommitByBranchName({
streamId: updatableStream.id,
branchName: 'main',
message: 'first commit',
objectId: testObject.id,
authorId: userOne.id,
sourceApplication: 'tests',
totalChildrenCount: null,
parents: null
})
const su = await getStream({ streamId: updatableStream.id })
expect(su?.updatedAt).to.be.ok
expect(su!.updatedAt).to.not.equal(updatableStream.updatedAt)
})
})
describe('when reading streams', () => {
const PAGE_LIMIT = 5
// keep owned+shared below maximum limit (50)
const OWNED_STREAM_COUNT = 30
const SHARED_STREAM_COUNT = 6
const TOTAL_OWN_STREAM_COUNT = OWNED_STREAM_COUNT + SHARED_STREAM_COUNT
const PUBLIC_STREAM_COUNT = 15
const DISCOVERABLE_STREAM_COUNT = PUBLIC_STREAM_COUNT - 5
let userOneStreams: BasicTestStream[]
let userTwoStreams: BasicTestStream[]
before(async () => {
// truncating previous streams
await truncateTables([Streams.name])
async function setupStreams(user: BasicTestUser): Promise<BasicTestStream[]> {
let remainingPublicStreams = PUBLIC_STREAM_COUNT
let remainingDiscoverableStreams = DISCOVERABLE_STREAM_COUNT
// creating test streams
const streamDefinitions = times(
OWNED_STREAM_COUNT,
(i): BasicTestStream => ({
name: `${user.name} test stream #${i}`,
isPublic: remainingPublicStreams-- > 0,
isDiscoverable: remainingDiscoverableStreams-- > 0,
id: '',
ownerId: ''
})
)
// invoking promises sequentially to ensure timestamps differ
for (const streamDef of streamDefinitions) {
await createTestStream(streamDef, user)
await sleep(1)
}
return streamDefinitions
}
async function shareStreams(
streams: BasicTestStream[],
streamOwner: BasicTestUser,
targetUser: BasicTestUser
) {
// invoking promises sequentially to ensure timestamps differ between items
for (let i = 0; i < SHARED_STREAM_COUNT; i++) {
await addOrUpdateStreamCollaborator(
streams[i].id,
targetUser.id,
Roles.Stream.Contributor,
streamOwner.id
)
await sleep(1)
}
}
// creating test streams
userOneStreams = await setupStreams(userOne)
userTwoStreams = await setupStreams(userTwo)
// share streams
await shareStreams(userOneStreams, userOne, userTwo)
await shareStreams(userTwoStreams, userTwo, userOne)
})
const paginationDataset = [
{ display: 'with pagination', pagination: true },
{ display: 'without pagination', pagination: false }
]
const isLimitedUserStreams = (
data: GetLimitedUserStreamsQuery | GetUserStreamsQuery
): data is GetLimitedUserStreamsQuery => has(data, 'otherUser')
/**
* Base test for testing paginated & unpaginated User.streams query in various circumstances
*/
const testPaginatedUserStreams = async (
apollo: ApolloServer,
pagination: boolean,
userId: string,
isOtherUser: boolean,
options: Partial<{ limitedUserQuery: boolean }> = {}
) => {
const { limitedUserQuery } = options
const expectedTotalCount = isOtherUser
? SHARED_STREAM_COUNT + DISCOVERABLE_STREAM_COUNT // only shared streams + discoverable ones
: TOTAL_OWN_STREAM_COUNT // all owned & shared streams
const requestPage = async (cursor?: Nullable<string>) => {
const vars = {
userId,
limit: pagination ? PAGE_LIMIT : 100,
cursor
}
const results = limitedUserQuery
? await getLimitedUserStreams(apollo, vars)
: await getUserStreams(apollo, vars)
expect(results).to.not.haveGraphQLErrors()
if (!results.data) throw new Error('Unexpected issue')
let streams: Get<GetUserStreamsQuery, 'user.streams'>
if (isLimitedUserStreams(results.data)) {
streams = results.data.otherUser?.streams
} else {
streams = results.data.user?.streams
}
if (!streams) throw new Error('Unexpected issue')
expect(streams.totalCount).to.eq(expectedTotalCount)
return streams
}
let cursor: Nullable<string> = null
let failSafe = Math.ceil(TOTAL_OWN_STREAM_COUNT / PAGE_LIMIT)
let allItemsFound = false
let foundItemsCount = 0
let foundOwnedStreams = 0
let foundSharedStreams = 0
let previousUpdatedAt: Nullable<Dayjs> = null
do {
const pageStreams: Awaited<ReturnType<typeof requestPage>> = await requestPage(
cursor
)
cursor = pageStreams.cursor || null
foundItemsCount += pageStreams.items?.length || 0
if (!pageStreams.items?.length) {
allItemsFound = true
break
}
for (const item of pageStreams.items || []) {
expect(item.id).to.be.ok
expect(item.role).to.be.ok
expect(item.createdAt).to.be.ok
expect(item.updatedAt).to.be.ok
const newUpdatedAt = dayjs(item.updatedAt)
if (previousUpdatedAt) {
const isSortingCorrect = previousUpdatedAt.isAfter(newUpdatedAt)
expect(isSortingCorrect).to.be.true
}
previousUpdatedAt = newUpdatedAt
if (item.role === Roles.Stream.Owner) {
foundOwnedStreams++
} else {
foundSharedStreams++
}
}
} while (failSafe-- > 0)
expect(allItemsFound).to.be.true
expect(foundItemsCount).to.eq(expectedTotalCount)
expect(foundOwnedStreams).to.eq(
isOtherUser
? DISCOVERABLE_STREAM_COUNT // only discoverable streams found, those user will be an owner in (see before())
: OWNED_STREAM_COUNT // all streams where user is a contributor
)
expect(foundSharedStreams).to.eq(SHARED_STREAM_COUNT)
}
describe('and user is authenticated', () => {
let apollo: ApolloServer
let activeUserId: string
before(async () => {
activeUserId = userOne.id
apollo = await buildAuthenticatedApolloServer(activeUserId)
})
paginationDataset.forEach(({ display, pagination }) => {
it(`User.streams() ${display} for active user returns all streams the user is a collaborator on`, async () => {
await testPaginatedUserStreams(apollo, pagination, activeUserId, false)
})
userLimitedUserDataSet.forEach(({ limitedUser }) => {
const prefix = limitedUser
? 'LimitedUser.streams()'
: 'User.streams() for a different user'
it(`${prefix} ${display} returns that users discoverable streams`, async () => {
await testPaginatedUserStreams(apollo, pagination, userTwo.id, true, {
limitedUserQuery: limitedUser
})
})
})
})
})
describe('and user is not authenticated', () => {
let apollo: ApolloServer
before(async () => {
apollo = await buildUnauthenticatedApolloServer()
})
userLimitedUserDataSet.forEach(({ display, limitedUser }) => {
it(`${display}.streams is inaccessible`, async () => {
const results = limitedUser
? await getLimitedUserStreams(apollo, { userId: userOne.id })
: await getUserStreams(apollo, { userId: userOne.id })
expect(results).to.haveGraphQLErrors()
expect(results.data?.otherUser || results.data?.user).to.be.not.ok
})
})
})
})
})