Files
speckle-server/packages/server/modules/auth/tests/integration/registration.spec.ts
T
Kristaps Fabians Geikins bde148f286 chore(server): migrating fully to ESM (#5042)
* wip

* some extra fixes

* stuff kinda works?

* need to figure out mocks

* need to figure out mocks

* fix db listener

* gqlgen fix

* minor gqlgen watch adjustment

* lint fixes

* delete old codegen file

* converting migrations to ESM

* getModuleDIrectory

* vitest sort of works

* added back ts-vitest

* resolve gql double load

* fixing test timeout configs

* TSC lint fix

* fix automate tests

* moar debugging

* debugging

* more debugging

* codegen update

* server works

* yargs migrated

* chore(server): getting rid of global mocks for Server ESM (#5046)

* got rid of email mock

* got rid of comment mocks

* got rid of multi region mocks

* got rid of stripe mock

* admin override mock updated

* removed final mock

* fixing import.meta.resolve calls

* another import.meta.resolve fix

* added requested test

* nyc ESM fix

* removed unneeded deps + linting

* yarn lock forgot to commit

* tryna fix flakyness

* email capture util fix

* sendEmail fix

* fix TSX check

* sender transporter fix + CR comments

* merge main fix

* test fixx

* circleci fix

* gqlgen bigint fix

* error formatter fix

* more error formatting improvements

* esmloader added to Dockerfile

* more dockerfile fixes

* bg jobs fix
2025-07-14 10:26:19 +03:00

309 lines
9.5 KiB
TypeScript

import { db } from '@/db/knex'
import {
generateRegistrationParams,
localAuthRestApi,
LocalAuthRestApiHelpers,
LoginParams
} from '@/modules/auth/tests/helpers/registration'
import { AllScopes } from '@/modules/core/helpers/mainConstants'
import { updateServerInfoFactory } from '@/modules/core/repositories/server'
import { findInviteFactory } from '@/modules/serverinvites/repositories/serverInvites'
import { LogicError } from '@/modules/shared/errors'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { expectToThrow, itEach } from '@/test/assertionHelper'
import { BasicTestUser, createTestUsers } from '@/test/authHelper'
import {
CreateProjectInviteDocument,
CreateProjectInviteMutationVariables,
CreateServerInviteDocument,
CreateServerInviteMutationVariables,
UseStreamInviteDocument
} from '@/test/graphql/generated/graphql'
import {
createTestContext,
testApolloServer,
TestApolloServer
} from '@/test/graphqlHelper'
import { beforeEachContext } from '@/test/hooks'
import { captureCreatedInvite } from '@/test/speckle-helpers/inviteHelper'
import {
BasicTestStream,
createTestStreams,
getUserStreamRole
} from '@/test/speckle-helpers/streamHelper'
import { Roles } from '@speckle/shared'
import { expect } from 'chai'
const { FF_NO_PERSONAL_EMAILS_ENABLED, FF_PERSONAL_PROJECTS_LIMITS_ENABLED } =
getFeatureFlags()
const updateServerInfo = updateServerInfoFactory({ db })
describe('Server registration', () => {
let restApi: LocalAuthRestApiHelpers
let apollo: TestApolloServer
const createInviteAsAdmin = async (
args: CreateProjectInviteMutationVariables | CreateServerInviteMutationVariables
) => {
return await captureCreatedInvite(async () => {
if ('projectId' in args) {
if (FF_PERSONAL_PROJECTS_LIMITS_ENABLED) {
throw new LogicError('Should not be invoked when personal limits are enabled')
}
await apollo.execute(CreateProjectInviteDocument, args, {
assertNoErrors: true
})
} else {
await apollo.execute(CreateServerInviteDocument, args, {
assertNoErrors: true
})
}
})
}
const basicAdminUser: BasicTestUser = {
name: 'Some Admin Guy',
email: 'admindude123@asdasd.com',
id: ''
}
const basicAdminStream: BasicTestStream = {
name: 'Admin Stream',
description: 'Admin Stream Description',
isPublic: true,
ownerId: '',
id: ''
}
before(async () => {
const ctx = await beforeEachContext()
restApi = localAuthRestApi({ express: ctx.app })
await createTestUsers([basicAdminUser])
await createTestStreams([[basicAdminStream, basicAdminUser]])
apollo = await testApolloServer({
authUserId: basicAdminUser.id
})
})
describe('with local strategy (email/pw)', () => {
it('works', async () => {
const challenge = 'asd123'
const params = generateRegistrationParams()
params.challenge = challenge
const user = await restApi.register(params)
// email remains unverified
expect(user.emails.every((e) => !e.verified)).to.be.true
})
FF_NO_PERSONAL_EMAILS_ENABLED
? it('rejects registration with blocked email domain', async () => {
const params = generateRegistrationParams()
params.user.email = 'test@gmail.com'
const error = await expectToThrow(() => restApi.register(params))
expect(error.message).to.contain(
'Please use your work email instead of a personal email address'
)
})
: null
it('fails without challenge', async () => {
const params = generateRegistrationParams()
params.challenge = ''
const e = await expectToThrow(async () => await restApi.register(params))
expect(e.message).to.contain('no challenge detected')
})
itEach(
<const>[
{ key: 'email', msg: 'E-mail address is required' },
{ key: 'name', msg: 'User name is required' },
{ key: 'password', msg: 'Password missing' }
],
({ key }) => `fails with empty ${key}`,
async ({ key, msg }) => {
const params = generateRegistrationParams()
params.user[key] = ''
const e = await expectToThrow(async () => await restApi.register(params))
expect(e.message).to.contain(msg)
}
)
it('fails with invalid invite token', async () => {
const params = generateRegistrationParams()
params.inviteToken = 'bababa'
const e = await expectToThrow(async () => await restApi.register(params))
expect(e.message).to.contain('Wrong e-mail address or invite token')
})
it('fails with mismatched challenge', async () => {
const params = generateRegistrationParams()
const e = await expectToThrow(
async () =>
await restApi.register(params, {
getTokenFromAccessCodeChallenge: 'mismatched'
})
)
expect(e.message).to.contain('Code challenge mismatch')
})
;(FF_PERSONAL_PROJECTS_LIMITS_ENABLED ? it.skip : it)(
'works with stream invite and allows joining stream afterwards',
async () => {
const params = generateRegistrationParams()
const invite = await createInviteAsAdmin({
input: {
email: params.user.email,
serverRole: Roles.Server.Admin
},
projectId: basicAdminStream.id
})
expect(invite.token).to.be.ok
params.inviteToken = invite.token
const newUser = await restApi.register(params)
expect(newUser.role).to.equal(Roles.Server.Admin)
const res = await apollo.execute(
UseStreamInviteDocument,
{
accept: true,
token: invite.token,
streamId: basicAdminStream.id
},
{
context: await createTestContext({
userId: newUser.id,
auth: true,
role: Roles.Server.User,
token: 'asd',
scopes: AllScopes
})
}
)
expect(res).to.not.haveGraphQLErrors()
expect(res.data?.streamInviteUse).to.be.ok
expect(await findInviteFactory({ db })({ inviteId: invite.id })).to.be.not.ok
const userStreamRole = await getUserStreamRole(newUser.id, basicAdminStream.id)
expect(userStreamRole).to.be.ok
}
)
const inviteOnlyModeSettings = [{ inviteOnly: true }, { inviteOnly: false }]
inviteOnlyModeSettings.forEach(({ inviteOnly }) => {
describe(`with inviteOnly mode ${inviteOnly ? 'enabled' : 'disabled'}`, () => {
before(async () => {
await updateServerInfo({ inviteOnly })
})
after(async () => {
await updateServerInfo({ inviteOnly: false })
})
if (inviteOnly) {
it('fails without invite token', async () => {
const params = generateRegistrationParams()
const e = await expectToThrow(async () => await restApi.register(params))
expect(e.message).to.contain('This server is invite only')
})
}
itEach(
[
...(FF_PERSONAL_PROJECTS_LIMITS_ENABLED ? [] : [{ stream: true }]),
{ stream: false }
],
({ stream }) =>
`works with valid ${
stream ? 'stream' : 'server'
} invite token and auto-verifies email`,
async ({ stream }) => {
const challenge = 'bababooey'
const params = generateRegistrationParams()
params.challenge = challenge
const invite = await createInviteAsAdmin({
input: {
email: params.user.email,
serverRole: Roles.Server.Admin
},
...(stream ? { projectId: basicAdminStream.id } : {})
})
expect(invite.token).to.be.ok
params.inviteToken = invite.token
const newUser = await restApi.register(params)
expect(newUser.role).to.equal(Roles.Server.Admin)
expect(newUser.emails.every((e) => e.verified)).to.be.true
}
)
})
})
describe('when logging in', () => {
const registeredUserParams = generateRegistrationParams()
before(async () => {
await restApi.register(registeredUserParams)
})
it('works with valid credentials', async () => {
const challenge = 'asd123asdasd'
const loginParams: LoginParams = {
email: registeredUserParams.user.email,
password: registeredUserParams.user.password,
challenge
}
await restApi.login(loginParams)
})
it("doesn't work with invalid challenge for 2nd call", async () => {
const challenge = 'asd123asdasd'
const loginParams: LoginParams = {
email: registeredUserParams.user.email,
password: registeredUserParams.user.password,
challenge
}
const e = await expectToThrow(async () => {
await restApi.login(loginParams, {
getTokenFromAccessCodeChallenge: 'mismatched'
})
})
expect(e.message).to.contain('Code challenge mismatch')
})
it("doesn't work with invalid credentials", async () => {
const challenge = 'asd123asdasd'
const loginParams: LoginParams = {
email: registeredUserParams.user.email,
password: 'wrongpassword',
challenge
}
const e = await expectToThrow(async () => await restApi.login(loginParams))
expect(e.message).to.contain('Invalid credentials')
})
})
})
})