feat(multiregion): replace user replication (#5253)

This commit is contained in:
Chuck Driesler
2025-08-28 09:02:53 +01:00
committed by GitHub
parent 2e30c34cd9
commit 8a9b4829d9
62 changed files with 1506 additions and 2444 deletions
@@ -16,28 +16,7 @@ import {
getStreamRolesFactory,
grantStreamPermissionsFactory
} from '@/modules/core/repositories/streams'
import {
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { getUserFactory } from '@/modules/core/repositories/users'
import { createPersonalAccessTokenFactory } from '@/modules/core/services/tokens'
import {
storePersonalApiTokenFactory,
@@ -45,7 +24,6 @@ import {
storeTokenScopesFactory,
storeTokenResourceAccessDefinitionsFactory
} from '@/modules/core/repositories/tokens'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { createObjectFactory } from '@/modules/core/services/objects/management'
import { storeSingleObjectIfNotFoundFactory } from '@/modules/core/repositories/objects'
import { getEventBus } from '@/modules/shared/services/eventBus'
@@ -55,6 +33,8 @@ 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 })
@@ -67,34 +47,6 @@ const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({
emitEvent: getEventBus().emit
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo: getServerInfoFactory({ db }),
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo: getServerInfoFactory({ db }),
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const createPersonalAccessToken = createPersonalAccessTokenFactory({
storeApiToken: storeApiTokenFactory({ db }),
storeTokenScopes: storeTokenScopesFactory({ db }),
@@ -113,29 +65,13 @@ let server: http.Server
let sendRequest: Awaited<ReturnType<typeof initializeTestServer>>['sendRequest']
describe('Activity @activity', () => {
const userIz = {
name: 'Izzy Lyseggen',
email: 'izzybizzi@speckle.systems',
password: 'sp0ckle sucks 9001',
id: '',
token: ''
}
let userIz: BasicTestUser
let userCr: BasicTestUser
let userX: BasicTestUser
const userCr = {
name: 'Cristi Balas',
email: 'cristib@speckle.systems',
password: 'hack3r man 666',
id: '',
token: ''
}
const userX = {
name: 'Mystery User',
email: 'mysteriousDude@speckle.systems',
password: 'super $ecret pw0rd',
id: '',
token: ''
}
let userIzToken: string
let userCrToken: string
let userXToken: string
const streamPublic: BasicTestStream = {
name: 'a fun stream for sharing',
@@ -197,30 +133,42 @@ describe('Activity @activity', () => {
]
// create users
await Promise.all([
createUser(userIz).then((id) => (userIz.id = id)),
createUser(userCr).then((id) => (userCr.id = id)),
createUser(userX).then((id) => (userX.id = id))
])
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'
})
// create tokens and streams
await Promise.all([
// tokens
createPersonalAccessToken(userIz.id, 'izz test token', normalScopesList).then(
(token) => (userIz.token = `Bearer ${token}`)
),
createPersonalAccessToken(userCr.id, 'cristi test token', normalScopesList).then(
(token) => (userCr.token = `Bearer ${token}`)
),
createPersonalAccessToken(userX.id, 'no users:read test token', [
Scopes.Streams.Read,
Scopes.Streams.Write
]).then((token) => (userX.token = `Bearer ${token}`))
// streams
// createStream({ ...collaboratorTestStream, ownerId: userIz.id }).then(
// (id) => (collaboratorTestStream.id = id)
// )
])
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
@@ -232,7 +180,7 @@ describe('Activity @activity', () => {
// create commit (cr2)
testObj2.id = await createObject({ streamId: streamSecret.id, object: testObj2 })
const resCommit1 = await sendRequest(userCr.token, {
const resCommit1 = await sendRequest(userCrToken, {
query: `mutation { commitCreate(commit: {streamId: "${streamSecret.id}", branchName: "main", objectId: "${testObj2.id}", message: "first commit"})}`
})
expect(noErrors(resCommit1))
@@ -249,7 +197,7 @@ describe('Activity @activity', () => {
// create commit #2 (iz3)
testObj.id = await createObject({ streamId: streamPublic.id, object: testObj })
const resCommit2 = await sendRequest(userIz.token, {
const resCommit2 = await sendRequest(userIzToken, {
query: `mutation { commitCreate(commit: { streamId: "${streamPublic.id}", branchName: "${branchPublic.name}", objectId: "${testObj.id}", message: "first commit" })}`
})
expect(noErrors(resCommit2))
@@ -263,7 +211,7 @@ describe('Activity @activity', () => {
)
// update collaborator (iz4)
const resCollab = await sendRequest(userIz.token, {
const resCollab = await sendRequest(userIzToken, {
query: `mutation { streamUpdatePermission( permissionParams: { streamId: "${streamPublic.id}", userId: "${userCr.id}", role: "stream:contributor" } ) }`
})
expect(noErrors(resCollab))
@@ -312,7 +260,7 @@ describe('Activity @activity', () => {
})
it("Should get a user's own activity", async () => {
const res = await sendRequest(userIz.token, {
const res = await sendRequest(userIzToken, {
query: `query {activeUser { name activity { totalCount items {id streamId resourceType resourceId actionType userId message time}}} }`
})
expect(noErrors(res))
@@ -325,7 +273,7 @@ describe('Activity @activity', () => {
})
it("Should get another user's activity", async () => {
const res = await sendRequest(userIz.token, {
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))
@@ -334,7 +282,7 @@ describe('Activity @activity', () => {
})
it("Should get a user's timeline", async () => {
const res = await sendRequest(userIz.token, {
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))
@@ -343,7 +291,7 @@ describe('Activity @activity', () => {
})
it("Should get a stream's activity", async () => {
const res = await sendRequest(userCr.token, {
const res = await sendRequest(userCrToken, {
query: `query { stream(id: "${streamPublic.id}") { activity { totalCount items {id streamId resourceId actionType message} } } }`
})
expect(noErrors(res))
@@ -354,7 +302,7 @@ describe('Activity @activity', () => {
})
it("Should get a branch's activity", async () => {
const res = await sendRequest(userCr.token, {
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))
@@ -365,7 +313,7 @@ describe('Activity @activity', () => {
})
it("Should *not* get a stream's activity if you don't have access to it", async () => {
const res = await sendRequest(userIz.token, {
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)
@@ -379,16 +327,18 @@ describe('Activity @activity', () => {
})
it("Should *not* get a user's activity without the `users:read` scope", async () => {
const res = await sendRequest(userX.token, {
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.errors?.length).to.equal(1)
expect(res.body.error).to.exist
})
it("Should *not* get a user's timeline without the `users:read` scope", async () => {
const res = await sendRequest(userX.token, {
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.errors?.length).to.equal(1)
expect(res.body.error).to.exist
})
})
+60 -38
View File
@@ -1,7 +1,7 @@
import type { SpeckleModule } from '@/modules/shared/helpers/typeHelper'
import { registerOrUpdateScopeFactory } from '@/modules/shared/repositories/scopes'
import { moduleLogger } from '@/observability/logging'
import { logger, moduleLogger } from '@/observability/logging'
import db from '@/db/knex'
import { initializeDefaultAppsFactory } from '@/modules/auth/services/serverApps'
import {
@@ -60,42 +60,9 @@ import { sendEmail } from '@/modules/emails/services/sending'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { isRateLimiterEnabled } from '@/modules/shared/helpers/envHelper'
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo: getServerInfoFactory({ db }),
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({
db
}),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo: getServerInfoFactory({ db }),
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const findOrCreateUser = findOrCreateUserFactory({
createUser,
findPrimaryEmailForUser: findPrimaryEmailForUserFactory({ db })
})
import { getAllRegisteredDbs } from '@/modules/multiregion/utils/dbSelector'
import type { CreateValidatedUser } from '@/modules/core/domain/users/operations'
import { asMultiregionalOperation } from '@/modules/shared/command'
const initializeDefaultApps = initializeDefaultAppsFactory({
getAllScopes: getAllScopesFactory({ db }),
@@ -113,10 +80,65 @@ const finalizeInvitedServerRegistration = finalizeInvitedServerRegistrationFacto
})
const resolveAuthRedirectPath = resolveAuthRedirectPathFactory()
const createUser: CreateValidatedUser = async (...input) =>
asMultiregionalOperation(
async ({ mainDb, allDbs, emit }) => {
const createUser = createUserFactory({
getServerInfo: getServerInfoFactory({ db: mainDb }),
findEmail: findEmailFactory({ db: mainDb }),
storeUser: async (...params) => {
const [user] = await Promise.all(
allDbs.map((db) => storeUserFactory({ db })(...params))
)
return user
},
countAdminUsers: countAdminUsersFactory({ db: mainDb }),
storeUserAcl: storeUserAclFactory({ db: mainDb }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db: mainDb }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({
db: mainDb
}),
findEmail: findEmailFactory({ db: mainDb }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db: mainDb }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db: mainDb })
}),
requestNewEmailVerification: requestNewEmailVerificationFactory({
getServerInfo: getServerInfoFactory({ db }),
findEmail: findEmailFactory({ db: mainDb }),
getUser: getUserFactory({ db: mainDb }),
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory(
{
db: mainDb
}
),
renderEmail,
sendEmail
})
}),
emitEvent: emit
})
return createUser(...input)
},
{
dbs: await getAllRegisteredDbs(),
name: 'create user',
logger
}
)
const commonBuilderDeps = {
getServerInfo: getServerInfoFactory({ db }),
getUserByEmail: legacyGetUserByEmailFactory({ db }),
findOrCreateUser,
buildFindOrCreateUser: async () => {
return findOrCreateUserFactory({
createUser,
findPrimaryEmailForUser: findPrimaryEmailForUserFactory({ db })
})
},
validateServerInvite,
finalizeInvitedServerRegistration,
resolveAuthRedirectPath,
@@ -40,7 +40,7 @@ const azureAdStrategyBuilderFactory =
(deps: {
getServerInfo: GetServerInfo
getUserByEmail: LegacyGetUserByEmail
findOrCreateUser: FindOrCreateValidatedUser
buildFindOrCreateUser: () => Promise<FindOrCreateValidatedUser>
validateServerInvite: ValidateServerInvite
finalizeInvitedServerRegistration: FinalizeInvitedServerRegistration
resolveAuthRedirectPath: ResolveAuthRedirectPath
@@ -102,6 +102,8 @@ const azureAdStrategyBuilderFactory =
serverVersion: serverInfo.version
})
const findOrCreateUser = await deps.buildFindOrCreateUser()
try {
// This is the only strategy that does its own type for req.user - easier to force type cast for now
// than to refactor everything
@@ -130,7 +132,7 @@ const azureAdStrategyBuilderFactory =
// if there is an existing user, go ahead and log them in (regardless of
// whether the server is invite only or not).
if (existingUser) {
const myUser = await deps.findOrCreateUser({
const myUser = await findOrCreateUser({
user
})
// ID is used later for verifying access token
@@ -156,7 +158,7 @@ const azureAdStrategyBuilderFactory =
}
// create the user
const myUser = await deps.findOrCreateUser({
const myUser = await findOrCreateUser({
user: {
...user,
role: invite
@@ -44,7 +44,7 @@ const githubStrategyBuilderFactory =
(deps: {
getServerInfo: GetServerInfo
getUserByEmail: LegacyGetUserByEmail
findOrCreateUser: FindOrCreateValidatedUser
buildFindOrCreateUser: () => Promise<FindOrCreateValidatedUser>
validateServerInvite: ValidateServerInvite
finalizeInvitedServerRegistration: FinalizeInvitedServerRegistration
resolveAuthRedirectPath: ResolveAuthRedirectPath
@@ -91,6 +91,8 @@ const githubStrategyBuilderFactory =
serverVersion: serverInfo.version
})
const findOrCreateUser = await deps.buildFindOrCreateUser()
try {
const email = profile.emails?.[0].value
if (!email) {
@@ -115,7 +117,7 @@ const githubStrategyBuilderFactory =
// if there is an existing user, go ahead and log them in (regardless of
// whether the server is invite only or not).
if (existingUser) {
const myUser = await deps.findOrCreateUser({ user })
const myUser = await findOrCreateUser({ user })
return done(null, myUser)
}
@@ -133,7 +135,7 @@ const githubStrategyBuilderFactory =
}
// create the user
const myUser = await deps.findOrCreateUser({
const myUser = await findOrCreateUser({
user: {
...user,
role: invite
@@ -39,7 +39,7 @@ const googleStrategyBuilderFactory =
(deps: {
getServerInfo: GetServerInfo
getUserByEmail: LegacyGetUserByEmail
findOrCreateUser: FindOrCreateValidatedUser
buildFindOrCreateUser: () => Promise<FindOrCreateValidatedUser>
validateServerInvite: ValidateServerInvite
finalizeInvitedServerRegistration: FinalizeInvitedServerRegistration
resolveAuthRedirectPath: ResolveAuthRedirectPath
@@ -75,6 +75,7 @@ const googleStrategyBuilderFactory =
profileId: profile.id,
serverVersion: serverInfo.version
})
const findOrCreateUser = await deps.buildFindOrCreateUser()
try {
// seems very weird that the Google strategy is not parsing 'error' query params
@@ -117,7 +118,7 @@ const googleStrategyBuilderFactory =
// if there is an existing user, go ahead and log them in (regardless of
// whether the server is invite only or not).
if (existingUser) {
const myUser = await deps.findOrCreateUser({ user })
const myUser = await findOrCreateUser({ user })
return done(null, myUser)
}
@@ -135,7 +136,7 @@ const googleStrategyBuilderFactory =
}
// create the user
const myUser = await deps.findOrCreateUser({
const myUser = await findOrCreateUser({
user: {
...user,
role: invite
@@ -38,7 +38,7 @@ const oidcStrategyBuilderFactory =
(deps: {
getServerInfo: GetServerInfo
getUserByEmail: LegacyGetUserByEmail
findOrCreateUser: FindOrCreateValidatedUser
buildFindOrCreateUser: () => Promise<FindOrCreateValidatedUser>
validateServerInvite: ValidateServerInvite
finalizeInvitedServerRegistration: FinalizeInvitedServerRegistration
resolveAuthRedirectPath: ResolveAuthRedirectPath
@@ -78,6 +78,8 @@ const oidcStrategyBuilderFactory =
serverVersion: serverInfo.version
})
const findOrCreateUser = await deps.buildFindOrCreateUser()
// TODO: req.session.inviteId doesn't appear to exist, but i'm not removing it to not break things
const token: Optional<string> =
get(req.session, 'inviteId') || req.session.token
@@ -107,7 +109,7 @@ const oidcStrategyBuilderFactory =
// if there is an existing user, go ahead and log them in (regardless of
// whether the server is invite only or not).
if (existingUser) {
const myUser = await deps.findOrCreateUser({
const myUser = await findOrCreateUser({
user
})
@@ -128,7 +130,7 @@ const oidcStrategyBuilderFactory =
}
// create the user
const myUser = await deps.findOrCreateUser({
const myUser = await findOrCreateUser({
user: {
...user,
role: invite
@@ -20,28 +20,6 @@ import {
} from '@/modules/auth/repositories/apps'
import { db } from '@/db/knex'
import { createAppTokenFromAccessCodeFactory } from '@/modules/auth/services/serverApps'
import {
findEmailFactory,
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import {
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import {
storeApiTokenFactory,
storeTokenScopesFactory,
@@ -49,9 +27,7 @@ import {
storeUserServerAppTokenFactory,
storePersonalApiTokenFactory
} from '@/modules/core/repositories/tokens'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getEventBus } from '@/modules/shared/services/eventBus'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser, type BasicTestUser } from '@/test/authHelper'
let sendRequest: Awaited<ReturnType<typeof initializeTestServer>>['sendRequest']
@@ -73,34 +49,6 @@ const createAppTokenFromAccessCode = createAppTokenFromAccessCodeFactory({
createBareToken
})
const getServerInfo = getServerInfoFactory({ db })
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const createPersonalAccessToken = createPersonalAccessTokenFactory({
storeApiToken: storeApiTokenFactory({ db }),
storeTokenScopes: storeTokenScopesFactory({ db }),
@@ -119,28 +67,25 @@ describe('GraphQL @apps-api', () => {
before(async () => {
const ctx = await beforeEachContext()
;({ sendRequest } = await initializeTestServer(ctx))
testUser = {
testUser = await createTestUser({
name: 'Dimitrie Stefanescu',
email: 'didimitrie@example.org',
password: 'wtfwtfwtf',
id: ''
}
testUser.id = await createUser(testUser)
})
testToken = `Bearer ${await createPersonalAccessToken(testUser.id, 'test token', [
Scopes.Profile.Read,
Scopes.Apps.Read,
Scopes.Apps.Write
])}`
testUser2 = {
testUser2 = await createTestUser({
name: 'Mr. Mac',
email: 'steve@jobs.com',
password: 'wtfwtfwtf',
id: ''
}
testUser2.id = await createUser(testUser2)
})
testToken2 = `Bearer ${await createPersonalAccessToken(testUser2.id, 'test token', [
Scopes.Profile.Read,
Scopes.Apps.Read,
+11 -65
View File
@@ -31,29 +31,7 @@ import {
createAppTokenFromAccessCodeFactory,
refreshAppTokenFactory
} from '@/modules/auth/services/serverApps'
import {
findEmailFactory,
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import {
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory,
getUserRoleFactory
} from '@/modules/core/repositories/users'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { getUserRoleFactory } from '@/modules/core/repositories/users'
import {
storeApiTokenFactory,
storeTokenScopesFactory,
@@ -65,9 +43,7 @@ import {
getTokenResourceAccessDefinitionsByIdFactory,
updateApiTokenFactory
} from '@/modules/core/repositories/tokens'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getEventBus } from '@/modules/shared/services/eventBus'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser, type BasicTestUser } from '@/test/authHelper'
import type { AppScopes } from '@speckle/shared'
import { ensureError } from '@speckle/shared'
import type { ValidTokenResult } from '@/modules/core/helpers/types'
@@ -115,34 +91,6 @@ const refreshAppToken = refreshAppTokenFactory({
createBareToken
})
const getServerInfo = getServerInfoFactory({ db })
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const validateToken = validateTokenFactory({
revokeUserTokenById: revokeUserTokenByIdFactory({ db }),
getApiTokenById: getApiTokenByIdFactory({ db }),
@@ -156,16 +104,16 @@ const validateToken = validateTokenFactory({
})
describe('Services @apps-services', () => {
const actor: BasicTestUser = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie@example.org',
password: 'wtfwtfwtf',
id: ''
}
let actor: BasicTestUser
before(async () => {
await beforeEachContext()
actor.id = await createUser(actor)
actor = await createTestUser({
name: 'Dimitrie Stefanescu',
email: 'didimitrie@example.org',
password: 'wtfwtfwtf',
id: ''
})
})
it('Should register an app', async () => {
@@ -507,14 +455,12 @@ describe('Services @apps-services', () => {
redirectUrl: 'http://127.0.0.1:1335',
authorId: actor.id
})
const secondUser: BasicTestUser = {
const secondUser = await createTestUser({
name: 'Dimitrie Stefanescu',
email: 'didimitrie.wow@example.org',
password: 'wtfwtfwtf',
id: ''
}
secondUser.id = await createUser(secondUser)
})
const accessCode = await createAuthorizationCode({
appId: myTestApp.id,
userId: secondUser.id,
+10 -47
View File
@@ -31,9 +31,6 @@ import { createBranchFactory } from '@/modules/core/repositories/branches'
import {
getUsersFactory,
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory,
legacyGetUserByEmailFactory
} from '@/modules/core/repositories/users'
import {
@@ -45,7 +42,6 @@ import { requestNewEmailVerificationFactory } from '@/modules/emails/services/ve
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
finalizeInvitedServerRegistrationFactory,
@@ -76,6 +72,8 @@ import { UserInputError } from '@/modules/core/errors/userinput'
import { createRandomEmail } from '@/modules/core/helpers/testHelpers'
import cryptoRandomString from 'crypto-random-string'
import { getFrontendOrigin } from '@/modules/shared/helpers/envHelper'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
const getServerInfo = getServerInfoFactory({ db })
@@ -161,33 +159,6 @@ const createStream = legacyCreateStreamFactory({
})
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const getUserByEmail = legacyGetUserByEmailFactory({ db })
const updateServerInfo = updateServerInfoFactory({ db })
const logger = extendLoggerComponent(baseLogger, 'auth-tests')
@@ -201,20 +172,7 @@ describe('Auth @auth', () => {
describe('Local authN & authZ (token endpoints)', () => {
const registeredUserEmail = 'registered@speckle.systems'
const me: {
name: string
company: string
email: string
password: string
id?: string
} = {
name: 'dimitrie stefanescu',
company: 'speckle',
email: registeredUserEmail,
password: 'roll saving throws',
id: undefined
}
let me: BasicTestUser
const myPrivateStream: {
name: string
isPublic: boolean
@@ -231,8 +189,13 @@ describe('Auth @auth', () => {
;({ sendRequest } = await initializeTestServer(ctx))
// Register a user for testing login flows
const meId = await createUser(me)
me.id = meId
me = await createTestUser({
name: 'dimitrie stefanescu',
company: 'speckle',
email: registeredUserEmail,
password: 'roll saving throws',
id: undefined
})
// Create a test stream for testing stream invites
const myPrivateStreamId = await createStream({
@@ -31,13 +31,7 @@ import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/se
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { createBranchFactory } from '@/modules/core/repositories/branches'
import {
getUsersFactory,
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import { getUsersFactory, getUserFactory } from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
@@ -47,7 +41,6 @@ import { requestNewEmailVerificationFactory } from '@/modules/emails/services/ve
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
finalizeInvitedServerRegistrationFactory,
@@ -63,6 +56,8 @@ import {
validateStreamAccessFactory
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
const getServerInfo = getServerInfoFactory({ db })
@@ -146,47 +141,18 @@ const createStream = legacyCreateStreamFactory({
})
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
describe('Blobs graphql @blobstorage', () => {
let graphqlServer: ServerAndContext
const user = {
name: 'Baron Von Blubba',
email: 'zebarron@bubble.bobble',
password: 'bubblesAreMyBlobs',
id: ''
}
let user: BasicTestUser
before(async () => {
await truncateTables(['blob_storage', Users.name, Streams.name])
user.id = await createUser(user)
user = await createTestUser({
name: 'Baron Von Blubba',
email: 'zebarron@bubble.bobble',
password: 'bubblesAreMyBlobs',
id: ''
})
graphqlServer = {
apollo: await buildApolloServer(),
context: await createAuthedTestContext(user.id)
@@ -4,87 +4,29 @@ import { expect } from 'chai'
import { beforeEachContext, getMainTestRegionKeyIfMultiRegion } from '@/test/hooks'
import { Scopes } from '@/modules/core/helpers/mainConstants'
import { db } from '@/db/knex'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import {
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import { createTokenFactory } from '@/modules/core/services/tokens'
import {
storeApiTokenFactory,
storeTokenScopesFactory,
storeTokenResourceAccessDefinitionsFactory
} from '@/modules/core/repositories/tokens'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import type { BasicTestStream } from '@/test/speckle-helpers/streamHelper'
import { createTestStream } from '@/test/speckle-helpers/streamHelper'
import { waitForRegionUser } from '@/test/speckle-helpers/regions'
import { createTestWorkspace } from '@/modules/workspaces/tests/helpers/creation'
import { faker } from '@faker-js/faker'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser, type BasicTestUser } from '@/test/authHelper'
import cryptoRandomString from 'crypto-random-string'
import type { BlobStorageItem } from '@/modules/blobstorage/domain/types'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { fileURLToPath } from 'url'
const getServerInfo = getServerInfoFactory({ db })
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const createRandomUser = async (): Promise<BasicTestUser> => {
const userDetails = {
name: cryptoRandomString({ length: 10 }),
email: `${cryptoRandomString({ length: 10, type: 'url-safe' })}@example.org`,
password: cryptoRandomString({ length: 12 })
}
return {
...userDetails,
id: await createUser(userDetails)
}
return createTestUser(userDetails)
}
const createToken = createTokenFactory({
storeApiToken: storeApiTokenFactory({ db }),
@@ -53,30 +53,6 @@ import {
getStreamObjectsFactory
} from '@/modules/core/repositories/objects'
import { legacyUpdateStreamFactory } from '@/modules/core/services/streams/management'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { getEventBus } from '@/modules/shared/services/eventBus'
import {
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import {
findEmailFactory,
ensureNoPrimaryEmailForUserFactory,
createUserEmailFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { createObjectFactory } from '@/modules/core/services/objects/management'
import {
getViewerResourcesFromLegacyIdentifiersFactory,
@@ -84,8 +60,10 @@ import {
} from '@/modules/core/services/commit/viewerResources'
import type { SetNonNullable } from 'type-fest'
import { createProject } from '@/test/projectHelper'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
import { getEventBus } from '@/modules/shared/services/eventBus'
const getServerInfo = getServerInfoFactory({ db })
const markCommitStreamUpdated = markCommitStreamUpdatedFactory({ db })
const streamResourceCheck = streamResourceCheckFactory({
checkStreamResourceAccess: checkStreamResourceAccessFactory({ db })
@@ -139,33 +117,6 @@ const updateStream = legacyUpdateStreamFactory({
})
const grantPermissionsStream = grantStreamPermissionsFactory({ db })
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const createObject = createObjectFactory({
storeSingleObjectIfNotFoundFactory: storeSingleObjectIfNotFoundFactory({ db })
})
@@ -718,7 +669,7 @@ describe('Graphql @comments', () => {
// this user will be admin by default
// it will be used to create all resources, that the other actors can
// be tested against
const myTestActor = {
let myTestActor: BasicTestUser = {
name: 'Gergo Jedlicska',
email: 'gergo@jedlicska.com',
password: 'sn3aky-1337-b1m',
@@ -1011,11 +962,11 @@ describe('Graphql @comments', () => {
before(async () => {
await beforeEachContext()
myTestActor.id = await createUser(myTestActor)
myTestActor = await createTestUser(myTestActor)
await Promise.all(
[chadTheEngineer, archived].map((user) =>
createUser({ name: user.name, email: user.email, password: user.password })
.then((id) => (user.id = id))
createTestUser({ name: user.name, email: user.email, password: user.password })
.then(({ id }) => (user.id = id))
.catch((err) => {
throw err
})
@@ -21,7 +21,8 @@ import {
import { get, range } from 'lodash-es'
import { buildApolloServer } from '@/app'
import { AllScopes } from '@/modules/core/helpers/mainConstants'
import { createAuthTokenForUser } from '@/test/authHelper'
import type { BasicTestUser } from '@/test/authHelper'
import { createAuthTokenForUser, createTestUser } from '@/test/authHelper'
import type { UploadedBlob } from '@/test/blobHelper'
import { uploadBlob } from '@/test/blobHelper'
import { Comments } from '@/modules/core/dbSchema'
@@ -95,13 +96,7 @@ import {
import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection'
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import {
getUsersFactory,
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import { getUsersFactory, getUserFactory } from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
@@ -111,7 +106,6 @@ import { requestNewEmailVerificationFactory } from '@/modules/emails/services/ve
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
finalizeInvitedServerRegistrationFactory,
@@ -313,33 +307,6 @@ const createStream = legacyCreateStreamFactory({
createStreamReturnRecord
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const createObject = createObjectFactory({
storeSingleObjectIfNotFoundFactory: storeSingleObjectIfNotFoundFactory({ db })
})
@@ -360,19 +327,8 @@ describe('Comments @comments', () => {
let notificationsState: NotificationsStateManager
const user = {
name: 'The comment wizard',
email: 'comment@wizard.ry',
password: 'i did not like Rivendel wine :(',
id: ''
}
const otherUser = {
name: 'Fondalf The Brey',
email: 'totalnotfakegandalf87@mordor.com',
password: 'what gandalf puts in his pipe stays in his pipe',
id: ''
}
let user: BasicTestUser
let otherUser: BasicTestUser
const stream = {
name: 'Commented stream',
@@ -400,8 +356,18 @@ describe('Comments @comments', () => {
const { app: express } = await beforeEachContext()
app = express
user.id = await createUser(user)
otherUser.id = await createUser(otherUser)
user = await createTestUser({
name: 'The comment wizard',
email: 'comment@wizard.ry',
password: 'i did not like Rivendel wine :(',
id: ''
})
otherUser = await createTestUser({
name: 'Fondalf The Brey',
email: 'totalnotfakegandalf87@mordor.com',
password: 'what gandalf puts in his pipe stays in his pipe',
id: ''
})
stream.id = await createStream({ ...stream, ownerId: user.id })
@@ -40,7 +40,6 @@ import { dbLogger } from '@/observability/logging'
import { getAdminUsersListCollectionFactory } from '@/modules/core/services/users/legacyAdminUsersList'
import type { Resolvers } from '@/modules/core/graph/generated/graphql'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getEventBus } from '@/modules/shared/services/eventBus'
import {
getMailchimpStatus,
getMailchimpOnboardingIds
@@ -48,36 +47,19 @@ import {
import { updateMailchimpMemberTags } from '@/modules/auth/services/mailchimp'
import { withOperationLogging } from '@/observability/domain/businessLogging'
import { metaHelpers } from '@/modules/core/helpers/meta'
import { asOperation } from '@/modules/shared/command'
import { asMultiregionalOperation, asOperation } from '@/modules/shared/command'
import { setUserOnboardingChoicesFactory } from '@/modules/core/services/users/tracking'
import { getMixpanelClient } from '@/modules/shared/utils/mixpanel'
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
import { getUserWorkspaceSeatsFactory } from '@/modules/workspacesCore/repositories/workspaces'
import { queryAllProjectsFactory } from '@/modules/core/services/projects'
import { getAllRegisteredDbs } from '@/modules/multiregion/utils/dbSelector'
const getUser = legacyGetUserFactory({ db })
const getUserByEmail = legacyGetUserByEmailFactory({ db })
const updateUserAndNotify = updateUserAndNotifyFactory({
getUser: getUserFactory({ db }),
updateUser: updateUserFactory({ db }),
emitEvent: getEventBus().emit
})
const getServerInfo = getServerInfoFactory({ db })
const deleteUser = deleteUserFactory({
deleteStream: deleteStreamFactory({ db }),
logger: dbLogger,
isLastAdminUser: isLastAdminUserFactory({ db }),
getUserDeletableStreams: getUserDeletableStreamsFactory({ db }),
queryAllProjects: queryAllProjectsFactory({
getExplicitProjects: getExplicitProjects({ db })
}),
getUserWorkspaceSeats: getUserWorkspaceSeatsFactory({ db }),
deleteAllUserInvites: deleteAllUserInvitesFactory({ db }),
deleteUserRecord: deleteUserRecordFactory({ db }),
emitEvent: getEventBus().emit
})
const getUserRole = getUserRoleFactory({ db })
const changeUserRole = changeUserRoleFactory({
getServerInfo,
@@ -261,14 +243,31 @@ export default {
const logger = context.log.child({
userIdToOperateOn: context.userId
})
await withOperationLogging(
async () => await updateUserAndNotify(context.userId!, args.user),
await asMultiregionalOperation(
async ({ mainDb, allDbs, emit }) => {
const updateUserAndNotify = updateUserAndNotifyFactory({
getUser: getUserFactory({ db: mainDb }),
updateUser: async (...params) => {
const [res] = await Promise.all(
allDbs.map((db) => updateUserFactory({ db })(...params))
)
return res
},
emitEvent: emit
})
return await updateUserAndNotify(context.userId!, args.user)
},
{
dbs: await getAllRegisteredDbs(),
logger,
operationName: 'updateUser',
operationDescription: `Update user`
name: 'updateUser',
description: `Update user`
}
)
return true
},
@@ -299,14 +298,39 @@ export default {
const logger = context.log.child({
userIdToOperateOn: user.id
})
await withOperationLogging(
async () => await deleteUser(user.id, context.userId),
await asMultiregionalOperation(
({ mainDb, allDbs, emit }) => {
const deleteUser = deleteUserFactory({
deleteStream: deleteStreamFactory({ db: mainDb }),
logger: dbLogger,
isLastAdminUser: isLastAdminUserFactory({ db: mainDb }),
getUserDeletableStreams: getUserDeletableStreamsFactory({ db: mainDb }),
queryAllProjects: queryAllProjectsFactory({
getExplicitProjects: getExplicitProjects({ db: mainDb })
}),
getUserWorkspaceSeats: getUserWorkspaceSeatsFactory({ db: mainDb }),
deleteAllUserInvites: deleteAllUserInvitesFactory({ db: mainDb }),
deleteUserRecord: async (params) => {
const [res] = await Promise.all(
allDbs.map((db) => deleteUserRecordFactory({ db })(params))
)
return res
},
emitEvent: emit
})
return deleteUser(user.id, context.userId)
},
{
logger,
operationName: 'adminDeleteUser',
operationDescription: `Admin deletion of an user`
name: 'adminDeleteUser',
description: 'Admin deletion of an user',
dbs: await getAllRegisteredDbs()
}
)
return true
},
@@ -325,19 +349,40 @@ export default {
// Since I am paranoid, I'll leave them here too.
await throwForNotHavingServerRole(context, Roles.Server.Guest)
await validateScopes(context.scopes, Scopes.Profile.Delete)
await asMultiregionalOperation(
({ mainDb, allDbs, emit }) => {
const deleteUser = deleteUserFactory({
deleteStream: deleteStreamFactory({ db: mainDb }),
logger: dbLogger,
isLastAdminUser: isLastAdminUserFactory({ db: mainDb }),
getUserDeletableStreams: getUserDeletableStreamsFactory({ db: mainDb }),
queryAllProjects: queryAllProjectsFactory({
getExplicitProjects: getExplicitProjects({ db: mainDb })
}),
getUserWorkspaceSeats: getUserWorkspaceSeatsFactory({ db: mainDb }),
deleteAllUserInvites: deleteAllUserInvitesFactory({ db: mainDb }),
deleteUserRecord: async (params) => {
const [res] = await Promise.all(
allDbs.map((db) => deleteUserRecordFactory({ db })(params))
)
await withOperationLogging(
async () => await deleteUser(context.userId!, context.userId!),
return res
},
emitEvent: emit
})
return deleteUser(user.id, context.userId)
},
{
logger,
operationName: 'deleteUser',
operationDescription: `Delete user`
name: 'deleteUser',
description: 'Delete user',
dbs: await getAllRegisteredDbs()
}
)
return true
},
activeUserMutations: () => ({})
},
ActiveUserMutations: {
@@ -394,14 +439,31 @@ export default {
},
async update(_parent, args, context) {
const logger = context.log
const newUser = await withOperationLogging(
async () => await updateUserAndNotify(context.userId!, args.user),
const newUser = await asMultiregionalOperation(
async ({ mainDb, allDbs, emit }) => {
const updateUserAndNotify = updateUserAndNotifyFactory({
getUser: getUserFactory({ db: mainDb }),
updateUser: async (...params) => {
const [res] = await Promise.all(
allDbs.map((db) => updateUserFactory({ db })(...params))
)
return res
},
emitEvent: emit
})
return await updateUserAndNotify(context.userId!, args.user)
},
{
dbs: await getAllRegisteredDbs(),
logger,
operationName: 'updateUser',
operationDescription: 'Update user'
name: 'updateUser',
description: `Update user`
}
)
return newUser
},
meta: () => ({})
@@ -0,0 +1,23 @@
import type { Knex } from 'knex'
const tableName = 'users'
const colUuid = 'suuid'
const colCreatedAt = 'createdAt'
const colVerified = 'verified'
export async function up(knex: Knex): Promise<void> {
await knex.schema.raw(`
ALTER TABLE "${tableName}"
ALTER COLUMN "${colUuid}" DROP DEFAULT,
ALTER COLUMN "${colCreatedAt}" DROP DEFAULT,
ALTER COLUMN "${colVerified}" DROP DEFAULT;
`)
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(tableName, (table) => {
table.string(colUuid).defaultTo(knex.raw('gen_random_uuid()')).alter()
table.timestamp(colCreatedAt).defaultTo(knex.fn.now()).alter()
table.boolean(colVerified).defaultTo(false).alter()
})
}
@@ -21,8 +21,6 @@ import { UserValidationError } from '@/modules/core/errors/user'
import type { Knex } from 'knex'
import type { ServerRoles } from '@speckle/shared'
import { Roles } from '@speckle/shared'
import { updateUserEmailFactory } from '@/modules/core/repositories/userEmails'
import { markUserEmailAsVerifiedFactory } from '@/modules/core/services/users/emailVerification'
import type { UserWithOptionalRole } from '@/modules/core/domain/users/types'
import type {
BulkLookupUsers,
@@ -228,11 +226,7 @@ export const markUserAsVerifiedFactory =
[UserCols.verified]: true
})
const userEmailsUpdate = await markUserEmailAsVerifiedFactory({
updateUserEmail: updateUserEmailFactory({ db: deps.db })
})({ email: email.toLowerCase().trim() })
return !!(usersUpdate || userEmailsUpdate)
return !!usersUpdate
}
export const markOnboardingCompleteFactory =
@@ -285,13 +279,6 @@ export const updateUserFactory =
.where(Users.col.id, userId)
.update(update, '*')
if (update.email) {
await updateUserEmailFactory(deps)({
query: { userId, primary: true },
update: { email: update.email }
})
}
return newUser as Nullable<UserRecord>
}
@@ -54,6 +54,7 @@ import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
import { ProjectEvents } from '@/modules/core/domain/projects/events'
import type { QueryAllProjects } from '@/modules/core/domain/projects/operations'
import type { StreamWithOptionalRole } from '@/modules/core/repositories/streams'
import { v4 } from 'uuid'
const { FF_NO_PERSONAL_EMAILS_ENABLED } = getFeatureFlags()
@@ -169,11 +170,12 @@ export const createUserFactory =
const signUpCtx = user.signUpContext
let finalUser: typeof user &
Omit<NullableKeysToOptional<UserRecord>, 'suuid' | 'createdAt'> = {
let finalUser: typeof user & NullableKeysToOptional<UserRecord> = {
...user,
id: crs({ length: 10 }),
verified: user.verified || false
verified: user.verified || false,
createdAt: new Date(),
suuid: v4()
}
delete finalUser.signUpContext
@@ -207,7 +209,10 @@ export const createUserFactory =
'name',
'company',
'verified',
'avatar'
'avatar',
'verified',
'createdAt',
'suuid'
]) as typeof finalUser)
finalUser.email = finalUser.email.toLowerCase()
@@ -61,13 +61,7 @@ import {
import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection'
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import {
getUsersFactory,
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import { getUsersFactory, getUserFactory } from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
@@ -77,7 +71,6 @@ import { requestNewEmailVerificationFactory } from '@/modules/emails/services/ve
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
finalizeInvitedServerRegistrationFactory,
@@ -97,6 +90,8 @@ import {
validateStreamAccessFactory
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
const db = knex
@@ -217,34 +212,6 @@ const createStream = legacyCreateStreamFactory({
emitEvent: getEventBus().emit
})
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const getBranchesByStreamId = getPaginatedStreamBranchesFactory({
getPaginatedStreamBranchesPage: getPaginatedStreamBranchesPageFactory({ db }),
getStreamBranchCount: getStreamBranchCountFactory({ db })
@@ -254,13 +221,7 @@ const createObject = createObjectFactory({
})
describe('Branches @core-branches', () => {
const user = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie4342@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
let user: BasicTestUser
const stream = {
name: 'Test Stream References',
description: 'Whatever goes in here usually...',
@@ -278,7 +239,12 @@ describe('Branches @core-branches', () => {
before(async () => {
await beforeEachContext()
user.id = await createUser(user)
user = await createTestUser({
name: 'Dimitrie Stefanescu',
email: 'didimitrie4342@example.org',
password: 'sn3aky-1337-b1m',
id: ''
})
stream.id = await createStream({ ...stream, ownerId: user.id })
testObject.id = await createObject({ streamId: stream.id, object: testObject })
})
@@ -62,13 +62,7 @@ import {
import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection'
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import {
getUsersFactory,
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import { getUsersFactory, getUserFactory } from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
@@ -78,7 +72,6 @@ import { requestNewEmailVerificationFactory } from '@/modules/emails/services/ve
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
finalizeInvitedServerRegistrationFactory,
@@ -101,6 +94,8 @@ import {
validateStreamAccessFactory
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
const getServerInfo = getServerInfoFactory({ db })
@@ -232,33 +227,6 @@ const createStream = legacyCreateStreamFactory({
})
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const getCommitsByUserId = legacyGetPaginatedUserCommitsPage({ db })
const getCommitsByStreamId = legacyGetPaginatedStreamCommitsPageFactory({ db })
const getCommitsTotalCountByBranchName = getBranchCommitsTotalCountByNameFactory({
@@ -274,13 +242,7 @@ const createObject = createObjectFactory({
})
describe('Commits @core-commits', () => {
const user = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie4342@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
let user: BasicTestUser
const stream = {
name: 'Test Stream References',
description: 'Whatever goes in here usually...',
@@ -314,7 +276,12 @@ describe('Commits @core-commits', () => {
before(async () => {
await beforeEachContext()
user.id = await createUser(user)
user = await createTestUser({
name: 'Dimitrie Stefanescu',
email: 'didimitrie4342@example.org',
password: 'sn3aky-1337-b1m',
id: ''
})
stream.id = await createStream({ ...stream, ownerId: user.id })
const testObjectId = await createObject({ streamId: stream.id, object: testObject })
@@ -37,13 +37,7 @@ import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/se
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { createBranchFactory } from '@/modules/core/repositories/branches'
import {
getUsersFactory,
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import { getUsersFactory, getUserFactory } from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
@@ -53,7 +47,6 @@ import { requestNewEmailVerificationFactory } from '@/modules/emails/services/ve
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
finalizeInvitedServerRegistrationFactory,
@@ -69,6 +62,7 @@ import {
validateStreamAccessFactory
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import { createTestUser, type BasicTestUser } from '@/test/authHelper'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
const getServerInfo = getServerInfoFactory({ db })
@@ -152,34 +146,6 @@ const createStream = legacyCreateStreamFactory({
})
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
/**
* Cleaning up relevant tables
*/
@@ -274,27 +240,25 @@ describe('Favorite streams', () => {
isPublic: true,
id: ''
}
const me = {
name: 'Itsa Me',
email: 'me@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
const otherGuy = {
name: 'Some Other DUde',
email: 'otherguy@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
let me: BasicTestUser
let otherGuy: BasicTestUser
before(async function () {
await cleanup()
// Seeding
await Promise.all([
createUser(me).then((id) => (me.id = id)),
createUser(otherGuy).then((id) => (otherGuy.id = id))
])
me = await createTestUser({
name: 'Itsa Me',
email: 'me@example.org',
password: 'sn3aky-1337-b1m',
id: ''
})
otherGuy = await createTestUser({
name: 'Some Other DUde',
email: 'otherguy@example.org',
password: 'sn3aky-1337-b1m',
id: ''
})
await Promise.all([
createStream({ ...myPubStream, ownerId: me.id }).then(
@@ -34,13 +34,7 @@ import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/se
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { createBranchFactory } from '@/modules/core/repositories/branches'
import {
getUsersFactory,
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import { getUsersFactory, getUserFactory } from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
@@ -50,7 +44,6 @@ import { requestNewEmailVerificationFactory } from '@/modules/emails/services/ve
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
finalizeInvitedServerRegistrationFactory,
@@ -67,6 +60,8 @@ import {
validateStreamAccessFactory
} from '@/modules/core/services/streams/access'
import type { Request } from 'express'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
const buildFinalizeProjectInvite = () =>
@@ -149,33 +144,6 @@ const createStream = legacyCreateStreamFactory({
})
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const adminOverrideMock = mockAdminOverride()
describe('Generic AuthN & AuthZ controller tests', () => {
@@ -295,23 +263,23 @@ describe('Generic AuthN & AuthZ controller tests', () => {
isPublic: false,
id: ''
}
const serverOwner = {
name: 'Itsa Me',
email: 'me@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
const otherGuy = {
name: 'Some Other DUde',
email: 'otherguy@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
let serverOwner: BasicTestUser
let otherGuy: BasicTestUser
before(async function () {
// Seeding
serverOwner.id = await createUser(serverOwner)
otherGuy.id = await createUser(otherGuy)
serverOwner = await createTestUser({
name: 'Itsa Me',
email: 'me@example.org',
password: 'sn3aky-1337-b1m',
id: ''
})
otherGuy = await createTestUser({
name: 'Some Other DUde',
email: 'otherguy@example.org',
password: 'sn3aky-1337-b1m',
id: ''
})
await Promise.all([
createStream({ ...myStream, ownerId: serverOwner.id }).then(
File diff suppressed because it is too large Load Diff
@@ -4,72 +4,17 @@ import {
createRandomEmail,
createRandomPassword
} from '@/modules/core/helpers/testHelpers'
import {
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory,
findEmailFactory
} from '@/modules/core/repositories/userEmails'
import { db } from '@/db/knex'
import { testApolloServer } from '@/test/graphqlHelper'
import {
CreateWorkspaceDocument,
CreateWorkspaceProjectDocument
} from '@/modules/core/graph/generated/graphql'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import {
countAdminUsersFactory,
legacyGetUserFactory,
storeUserAclFactory,
storeUserFactory
} from '@/modules/core/repositories/users'
import { createUserFactory } from '@/modules/core/services/users/management'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { WorkspaceReadOnlyError } from '@/modules/gatekeeper/errors/billing'
import gql from 'graphql-tag'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { getEventBus } from '@/modules/shared/services/eventBus'
const getServerInfo = getServerInfoFactory({ db })
const getUser = legacyGetUserFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db }),
getUser,
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUserEmail = validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail: findEmailFactory({ db }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
})
const findEmail = findEmailFactory({ db })
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: createUserEmail,
emitEvent: getEventBus().emit
})
import { createTestUser } from '@/test/authHelper'
const { FF_BILLING_INTEGRATION_ENABLED } = getFeatureFlags()
@@ -87,7 +32,7 @@ describe('Commits graphql @core', () => {
;(FF_BILLING_INTEGRATION_ENABLED ? it : it.skip)(
'should return error if project is read-only',
async () => {
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'emails user',
email: createRandomEmail(),
password: createRandomPassword()
@@ -1,6 +1,4 @@
import {
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory,
findEmailFactory,
updateUserEmailFactory
} from '@/modules/core/repositories/userEmails'
@@ -11,60 +9,13 @@ import {
} from '@/modules/core/helpers/testHelpers'
import { markUserEmailAsVerifiedFactory } from '@/modules/core/services/users/emailVerification'
import { expect } from 'chai'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import {
countAdminUsersFactory,
getUserFactory,
storeUserAclFactory,
storeUserFactory
} from '@/modules/core/repositories/users'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getEventBus } from '@/modules/shared/services/eventBus'
const getServerInfo = getServerInfoFactory({ db })
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
import { createTestUser } from '@/test/authHelper'
describe('Verification @user-emails', () => {
it('should mark user email as verified', async () => {
const email = createRandomEmail()
await createUser({
await createTestUser({
name: 'John',
email,
password: createRandomPassword()
@@ -2,72 +2,22 @@ import {
createRandomEmail,
createRandomPassword
} from '@/modules/core/helpers/testHelpers'
import {
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory,
findEmailFactory,
updateUserEmailFactory
} from '@/modules/core/repositories/userEmails'
import { updateUserEmailFactory } from '@/modules/core/repositories/userEmails'
import { db } from '@/db/knex'
import { expect } from 'chai'
import {
bulkLookupUsersFactory,
countAdminUsersFactory,
getUserByEmailFactory,
getUserFactory,
getUsersFactory,
listUsersFactory,
lookupUsersFactory,
storeUserAclFactory,
storeUserFactory
lookupUsersFactory
} from '@/modules/core/repositories/users'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import type { BasicTestStream } from '@/test/speckle-helpers/streamHelper'
import { createTestStream } from '@/test/speckle-helpers/streamHelper'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { buildBasicTestUser, createTestUser } from '@/test/authHelper'
const getServerInfo = getServerInfoFactory({ db })
const getUsers = getUsersFactory({ db })
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const getUserByEmail = getUserByEmailFactory({ db })
const listUsers = listUsersFactory({ db })
const lookupUsers = lookupUsersFactory({ db })
@@ -79,7 +29,7 @@ describe('Find users @core', () => {
const email = createRandomEmail()
const password = createRandomPassword()
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'John Doe',
password,
email
@@ -103,7 +53,7 @@ describe('Find users @core', () => {
const email = createRandomEmail()
const password = createRandomPassword()
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'John Doe',
password,
email
@@ -129,7 +79,7 @@ describe('Find users @core', () => {
const email = createRandomEmail()
const password = createRandomPassword()
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'John Doe',
password,
email
@@ -155,11 +105,13 @@ describe('Find users @core', () => {
describe('getUserByEmail', () => {
it('should ignore email casing', async () => {
const email = 'TeST@ExamPLE.oRg'
await createUser({
name: 'John Doe',
password: createRandomPassword(),
email
})
await createTestUser(
buildBasicTestUser({
name: 'John Doe',
password: createRandomPassword(),
email
})
)
const user = await getUserByEmail(email)
expect(user!.email).to.equal(email.toLowerCase())
})
@@ -168,41 +120,49 @@ describe('Find users @core', () => {
describe('lookupUsers', () => {
it('should find matches by name', async () => {
const email = createRandomEmail()
const userId = await createUser({
email,
name: 'John Spackle',
password: createRandomPassword()
})
const { id: userId } = await createTestUser(
buildBasicTestUser({
email,
name: 'John Spackle',
password: createRandomPassword()
})
)
const { users } = await lookupUsers({ query: 'Spack' })
expect(users.some((user) => user.id === userId)).to.equal(true)
})
it('should not find matches by name if filtered to emails only', async () => {
const email = createRandomEmail()
const userId = await createUser({
email,
name: 'John Spackle',
password: createRandomPassword()
})
const { id: userId } = await createTestUser(
buildBasicTestUser({
email,
name: 'John Spackle',
password: createRandomPassword()
})
)
const { users } = await lookupUsers({ query: 'Spack', emailOnly: true })
expect(users.some((user) => user.id === userId)).to.equal(false)
})
it('should find matches by email', async () => {
const email = createRandomEmail()
const userId = await createUser({
email,
name: 'John Spackle',
password: createRandomPassword()
})
const { id: userId } = await createTestUser(
buildBasicTestUser({
email,
name: 'John Spackle',
password: createRandomPassword()
})
)
const { users } = await lookupUsers({ query: email })
expect(users.some((user) => user.id === userId)).to.equal(true)
})
it('should find matches by email, case insensitive', async () => {
const email = 'fooBAR@example.org'
const userId = await createUser({
email,
name: 'John Spackle',
password: createRandomPassword()
})
const { id: userId } = await createTestUser(
buildBasicTestUser({
email,
name: 'John Spackle',
password: createRandomPassword()
})
)
const { users } = await lookupUsers({ query: 'FoObAr@example.org' })
expect(users.some((user) => user.id === userId)).to.equal(true)
})
@@ -4,11 +4,6 @@ import {
createRandomEmail,
createRandomPassword
} from '@/modules/core/helpers/testHelpers'
import {
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory,
findEmailFactory
} from '@/modules/core/repositories/userEmails'
import { db } from '@/db/knex'
import { testApolloServer } from '@/test/graphqlHelper'
import {
@@ -16,61 +11,10 @@ import {
CreateWorkspaceDocument,
CreateWorkspaceProjectDocument
} from '@/modules/core/graph/generated/graphql'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import {
countAdminUsersFactory,
legacyGetUserFactory,
storeUserAclFactory,
storeUserFactory
} from '@/modules/core/repositories/users'
import { createUserFactory } from '@/modules/core/services/users/management'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { WorkspaceReadOnlyError } from '@/modules/gatekeeper/errors/billing'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { PaidWorkspacePlanStatuses } from '@speckle/shared'
const getServerInfo = getServerInfoFactory({ db })
const getUser = legacyGetUserFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db }),
getUser,
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUserEmail = validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail: findEmailFactory({ db }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
})
const findEmail = findEmailFactory({ db })
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: createUserEmail,
emitEvent: getEventBus().emit
})
import { createTestUser } from '@/test/authHelper'
const { FF_BILLING_INTEGRATION_ENABLED } = getFeatureFlags()
@@ -83,7 +27,7 @@ describe('Objects graphql @core', () => {
;(FF_BILLING_INTEGRATION_ENABLED ? it : it.skip)(
'should return error if project is read-only',
async () => {
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'emails user',
email: createRandomEmail(),
password: createRandomPassword()
@@ -3,29 +3,7 @@ import {
createRandomEmail,
createRandomPassword
} from '@/modules/core/helpers/testHelpers'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import {
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory,
findEmailFactory
} from '@/modules/core/repositories/userEmails'
import {
countAdminUsersFactory,
legacyGetUserFactory,
storeUserAclFactory,
storeUserFactory
} from '@/modules/core/repositories/users'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { createUserFactory } from '@/modules/core/services/users/management'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import { legacyGetUserFactory } from '@/modules/core/repositories/users'
import { createTestWorkspace } from '@/modules/workspaces/tests/helpers/creation'
import { beforeEachContext } from '@/test/hooks'
import type { BasicTestStream } from '@/test/speckle-helpers/streamHelper'
@@ -42,41 +20,9 @@ import {
import { PaidWorkspacePlans, Scopes } from '@speckle/shared'
import { expect } from 'chai'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { createTestUser } from '@/test/authHelper'
const getServerInfo = getServerInfoFactory({ db })
const getUser = legacyGetUserFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db }),
getUser,
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUserEmail = validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail: findEmailFactory({ db }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
})
const findEmail = findEmailFactory({ db })
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: createUserEmail,
emitEvent: getEventBus().emit
})
const createPersonalAccessToken = createPersonalAccessTokenFactory({
storeApiToken: storeApiTokenFactory({ db }),
storeTokenScopes: storeTokenScopesFactory({ db }),
@@ -96,7 +42,7 @@ describe('Objects REST @core', () => {
;(FF_BILLING_INTEGRATION_ENABLED ? it : it.skip)(
'should return an error if the project is read-only',
async () => {
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'emails user',
email: createRandomEmail(),
password: createRandomPassword()
@@ -6,29 +6,7 @@ import {
createRandomEmail,
createRandomPassword
} from '@/modules/core/helpers/testHelpers'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import {
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory,
findEmailFactory
} from '@/modules/core/repositories/userEmails'
import {
countAdminUsersFactory,
legacyGetUserFactory,
storeUserAclFactory,
storeUserFactory
} from '@/modules/core/repositories/users'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { createUserFactory } from '@/modules/core/services/users/management'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import { legacyGetUserFactory } from '@/modules/core/repositories/users'
import { beforeEachContext, initializeTestServer } from '@/test/hooks'
import type { BasicTestStream } from '@/test/speckle-helpers/streamHelper'
import { createTestStream } from '@/test/speckle-helpers/streamHelper'
@@ -40,7 +18,6 @@ import {
storeTokenScopesFactory
} from '@/modules/core/repositories/tokens'
import { Scopes } from '@speckle/shared'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { generateManyObjects } from '@/test/helpers'
import type { RawSpeckleObject } from '@/modules/core/domain/objects/types'
import { storeObjectsIfNotFoundFactory } from '@/modules/core/repositories/objects'
@@ -49,41 +26,11 @@ import type { Parser } from 'csv-parse'
import { parse } from 'csv-parse'
import { createReadStream } from 'fs'
import { createObjectsBatchedAndNoClosuresFactory } from '@/modules/core/services/objects/management'
import { createTestUser } from '@/test/authHelper'
const IS_NODE_22_OR_ABOVE = process.versions.node.split('.').map(Number)[0] >= 22
const getServerInfo = getServerInfoFactory({ db })
const getUser = legacyGetUserFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db }),
getUser,
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUserEmail = validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail: findEmailFactory({ db }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
})
const findEmail = findEmailFactory({ db })
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: createUserEmail,
emitEvent: getEventBus().emit
})
const createPersonalAccessToken = createPersonalAccessTokenFactory({
storeApiToken: storeApiTokenFactory({ db }),
@@ -106,7 +53,7 @@ describe('Objects streaming REST @core', () => {
;(IS_NODE_22_OR_ABOVE ? it : it.skip)(
'should close database connections if client connection is prematurely closed',
async () => {
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'emails user',
email: createRandomEmail(),
password: createRandomPassword()
@@ -160,7 +107,7 @@ describe('Objects streaming REST @core', () => {
;(IS_NODE_22_OR_ABOVE ? it : it.skip)(
'should stream model with some failing feature',
async () => {
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'emails user',
email: createRandomEmail(),
password: createRandomPassword()
@@ -5,66 +5,11 @@ import {
createRandomEmail,
createRandomPassword
} from '@/modules/core/helpers/testHelpers'
import { UserEmails } from '@/modules/core/dbSchema'
import {
countAdminUsersFactory,
getUserFactory,
legacyGetUserFactory,
storeUserAclFactory,
storeUserFactory,
updateUserFactory
} from '@/modules/core/repositories/users'
import { updateUserFactory } from '@/modules/core/repositories/users'
import { expectToThrow } from '@/test/assertionHelper'
import {
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory,
findEmailFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getEventBus } from '@/modules/shared/services/eventBus'
const userEmailsDB = db(UserEmails.name)
import { buildBasicTestUser, createTestUser } from '@/test/authHelper'
const getServerInfo = getServerInfoFactory({ db })
const getUser = legacyGetUserFactory({ db })
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const updateUser = updateUserFactory({ db })
describe('Users @core-users', () => {
@@ -85,23 +30,20 @@ describe('Users @core-users', () => {
expect(err.message).eq('User update payload empty')
})
it('Should update user email if skipClean is true', async () => {
const email = createRandomEmail()
const newUser = {
name: 'John Doe',
email,
password: createRandomPassword()
}
const userId = await createUser(newUser)
// this will never actually happen
it('updates the user email', async () => {
const { id: userId } = await createTestUser(
buildBasicTestUser({
name: 'John Doe',
email: createRandomEmail(),
password: createRandomPassword()
})
)
const newEmail = createRandomEmail()
await updateUser(userId, { email: newEmail }, { skipClean: true })
const updated = await getUser(userId)
const updatedUserEmail = await userEmailsDB.where({ userId, primary: true }).first()
const updated = await db('users').where({ id: userId }).first()
expect(updated.email.toLowerCase()).eq(newEmail.toLowerCase())
expect(updatedUserEmail.email.toLowerCase()).eq(newEmail.toLowerCase())
})
})
@@ -28,15 +28,8 @@ import { requestNewEmailVerificationFactory } from '@/modules/emails/services/ve
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import {
countAdminUsersFactory,
legacyGetUserFactory,
storeUserAclFactory,
storeUserFactory
} from '@/modules/core/repositories/users'
import { createUserFactory } from '@/modules/core/services/users/management'
import { legacyGetUserFactory } from '@/modules/core/repositories/users'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { createTestUser, login } from '@/test/authHelper'
import { EmailVerificationFinalizationError } from '@/modules/emails/errors'
import { Roles } from '@speckle/shared'
@@ -63,17 +56,6 @@ const createUserEmail = validateAndCreateUserEmailFactory({
requestNewEmailVerification
})
const findEmail = findEmailFactory({ db })
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: createUserEmail,
emitEvent: getEventBus().emit
})
describe('User emails graphql @core', () => {
before(async () => {
await beforeEachContext()
@@ -85,7 +67,7 @@ describe('User emails graphql @core', () => {
describe('createUserEmail mutation', () => {
it('should create new email for user', async () => {
const firstEmail = createRandomEmail()
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'emails user',
email: firstEmail,
password: createRandomPassword()
@@ -117,7 +99,7 @@ describe('User emails graphql @core', () => {
describe('deleteUserEmail mutation', () => {
it('should delete email for user', async () => {
const firstEmail = createRandomEmail()
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'emails user',
email: firstEmail,
password: createRandomPassword()
@@ -147,7 +129,7 @@ describe('User emails graphql @core', () => {
describe('setPrimaryUserEmail mutation', () => {
it('should set primary email for user', async () => {
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'emails user',
email: createRandomEmail(),
password: createRandomPassword()
@@ -1,16 +1,12 @@
import { beforeEachContext } from '@/test/hooks'
import { expect } from 'chai'
import {
countAdminUsersFactory,
getUserByEmailFactory,
getUserFactory,
legacyGetPaginatedUsersCountFactory,
legacyGetPaginatedUsersFactory,
legacyGetUserByEmailFactory,
listUsersFactory,
markUserAsVerifiedFactory,
storeUserAclFactory,
storeUserFactory
listUsersFactory
} from '@/modules/core/repositories/users'
import { db } from '@/db/knex'
import {
@@ -30,7 +26,7 @@ import {
import { expectToThrow } from '@/test/assertionHelper'
import type { MaybeNullOrUndefined } from '@speckle/shared'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUsers } from '@/test/authHelper'
import { createTestUser, createTestUsers } from '@/test/authHelper'
import { UserEmails, Users } from '@/modules/core/dbSchema'
import { UserEmailPrimaryUnverifiedError } from '@/modules/core/errors/userEmails'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
@@ -43,9 +39,8 @@ import { requestNewEmailVerificationFactory } from '@/modules/emails/services/ve
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { markUserEmailAsVerifiedFactory } from '@/modules/core/services/users/emailVerification'
const getServerInfo = getServerInfoFactory({ db })
const getUsers = legacyGetPaginatedUsersFactory({ db })
@@ -72,20 +67,12 @@ const createUserEmail = validateAndCreateUserEmailFactory({
requestNewEmailVerification
})
const findEmail = findEmailFactory({ db })
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: createUserEmail,
emitEvent: getEventBus().emit
})
const getUserByEmail = getUserByEmailFactory({ db })
const legacyGetUserByEmail = legacyGetUserByEmailFactory({ db })
const listUsers = listUsersFactory({ db })
const markUserAsVerified = markUserAsVerifiedFactory({ db })
const markUserEmailAsVerified = markUserEmailAsVerifiedFactory({
updateUserEmail: updateUserEmailFactory({ db })
})
describe('Core @user-emails', () => {
before(async () => {
@@ -100,13 +87,13 @@ describe('Core @user-emails', () => {
describe('markUserEmailAsVerified', () => {
it('should mark user email as verified', async () => {
const email = createRandomEmail()
await createUser({
await createTestUser({
name: 'John Doe',
email,
password: createRandomPassword()
})
await markUserAsVerified(email)
await markUserEmailAsVerified({ email })
const userEmail = await findEmailFactory({ db })({ email })
expect(userEmail?.verified).to.be.true
@@ -116,7 +103,7 @@ describe('Core @user-emails', () => {
describe('deleteUserEmail', () => {
it('should throw and error when trying to delete last email', async () => {
const email = createRandomEmail()
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'John Doe',
email,
password: createRandomPassword()
@@ -133,7 +120,7 @@ describe('Core @user-emails', () => {
it('should throw and error when trying to delete primary email', async () => {
const email = createRandomEmail()
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'John Doe',
email,
password: createRandomPassword()
@@ -157,7 +144,7 @@ describe('Core @user-emails', () => {
it('should delete email', async () => {
const email = createRandomEmail()
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'John Doe',
email: createRandomEmail(),
password: createRandomPassword()
@@ -198,7 +185,7 @@ describe('Core @user-emails', () => {
it('should throw an error if trying to set non verified email as primary', async () => {
const email1 = createRandomEmail()
const email2 = createRandomEmail()
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'John Doe',
email: email1,
password: createRandomPassword()
@@ -239,7 +226,7 @@ describe('Core @user-emails', () => {
})
it('should set primary email', async () => {
const email = createRandomEmail()
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'John Doe',
email: createRandomEmail(),
password: createRandomPassword()
@@ -284,7 +271,7 @@ describe('Core @user-emails', () => {
describe('validateAndCreateUserEmailFactory', () => {
it('should throw an error when trying to create a primary email for a user and there is already one for that user', async () => {
const email = createRandomEmail()
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'John Doe',
email: createRandomEmail(),
password: createRandomPassword()
@@ -304,12 +291,12 @@ describe('Core @user-emails', () => {
})
it('should throw an error when trying to create an email for a user and the same email is already on the server', async () => {
const email = createRandomEmail()
const userId1 = await createUser({
const { id: userId1 } = await createTestUser({
name: 'John Doe 2',
email: createRandomEmail(),
password: createRandomPassword()
})
const userId2 = await createUser({
const { id: userId2 } = await createTestUser({
name: 'John Doe',
email: createRandomEmail(),
password: createRandomPassword()
@@ -341,7 +328,7 @@ describe('Core @user-emails', () => {
describe('updateUserEmail', () => {
it('should throw an error when trying to mark an email as primary and there is already one for the user', async () => {
const email = createRandomEmail()
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'John Doe',
email: createRandomEmail(),
password: createRandomPassword()
@@ -486,7 +473,9 @@ describe('Core @user-emails', () => {
})
it('with markUserAsVerified()', async () => {
const res = await markUserAsVerified(randomizeCase(randomCaseGuy.email))
const res = await markUserEmailAsVerified({
email: randomizeCase(randomCaseGuy.email)
})
expect(res).to.be.ok
const user = await getUserByEmail(randomCaseGuy.email)
@@ -5,11 +5,6 @@ import {
createRandomPassword,
createRandomString
} from '@/modules/core/helpers/testHelpers'
import {
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory,
findEmailFactory
} from '@/modules/core/repositories/userEmails'
import { db } from '@/db/knex'
import { testApolloServer } from '@/test/graphqlHelper'
import {
@@ -20,28 +15,10 @@ import {
GetProjectWithModelVersionsDocument,
GetProjectWithVersionsDocument
} from '@/modules/core/graph/generated/graphql'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import {
countAdminUsersFactory,
legacyGetUserFactory,
storeUserAclFactory,
storeUserFactory
} from '@/modules/core/repositories/users'
import { createUserFactory } from '@/modules/core/services/users/management'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { WorkspaceReadOnlyError } from '@/modules/gatekeeper/errors/billing'
import type { CreateVersionInput } from '@/modules/core/graph/generated/graphql'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { getEventBus } from '@/modules/shared/services/eventBus'
import type { BasicTestUser } from '@/test/authHelper'
import { buildBasicTestUser, createTestUser, login } from '@/test/authHelper'
import type { BasicTestStream } from '@/test/speckle-helpers/streamHelper'
import { createTestStream } from '@/test/speckle-helpers/streamHelper'
@@ -62,39 +39,6 @@ import {
} from '@/modules/core/tests/helpers/creation'
import type { Optional } from '@speckle/shared'
const getServerInfo = getServerInfoFactory({ db })
const getUser = legacyGetUserFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db }),
getUser,
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUserEmail = validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail: findEmailFactory({ db }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
})
const findEmail = findEmailFactory({ db })
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: createUserEmail,
emitEvent: getEventBus().emit
})
const { FF_BILLING_INTEGRATION_ENABLED, FF_PERSONAL_PROJECTS_LIMITS_ENABLED } =
getFeatureFlags()
@@ -107,7 +51,7 @@ describe('Versions graphql @core', () => {
;(FF_BILLING_INTEGRATION_ENABLED ? it : it.skip)(
'should return error if project is read-only',
async () => {
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'emails user',
email: createRandomEmail(),
password: createRandomPassword()
@@ -156,7 +100,7 @@ describe('Versions graphql @core', () => {
createdAt: Date // Make the project read-only
) => await db('commits').update({ createdAt }).where({ id })
const user = buildBasicTestUser()
let user: BasicTestUser
const workspace = buildBasicTestWorkspace()
const model1 = buildBasicTestModel()
const model2 = buildBasicTestModel()
@@ -169,7 +113,7 @@ describe('Versions graphql @core', () => {
let objectId3: Optional<string> = undefined
before(async () => {
user.id = await createUser(user)
user = await createTestUser(buildBasicTestUser())
await createTestWorkspace(workspace, user, {
addPlan: { name: 'free', status: 'valid' }
})
@@ -32,13 +32,7 @@ import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/se
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { createBranchFactory } from '@/modules/core/repositories/branches'
import {
getUsersFactory,
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import { getUsersFactory, getUserFactory } from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
@@ -48,7 +42,6 @@ import { requestNewEmailVerificationFactory } from '@/modules/emails/services/ve
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
finalizeInvitedServerRegistrationFactory,
@@ -79,6 +72,7 @@ import {
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import type { ObjectRecord } from '@/modules/core/helpers/types'
import { createTestUser, type BasicTestUser } from '@/test/authHelper'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
const sampleCommit = JSON.parse(`{
@@ -184,34 +178,6 @@ const createStream = legacyCreateStreamFactory({
emitEvent: getEventBus().emit
})
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const createObject = createObjectFactory({
storeSingleObjectIfNotFoundFactory: storeSingleObjectIfNotFoundFactory({ db })
})
@@ -228,13 +194,7 @@ const getObjectChildrenQuery = getObjectChildrenQueryFactory({ db })
const getObjects = getStreamObjectsFactory({ db })
describe('Objects @core-objects', () => {
const userOne = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie43@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
let userOne: BasicTestUser
const stream = {
name: 'Test Streams',
description: 'Whatever goes in here usually...',
@@ -244,7 +204,12 @@ describe('Objects @core-objects', () => {
before(async () => {
await beforeEachContext()
userOne.id = await createUser(userOne)
userOne = await createTestUser({
name: 'Dimitrie Stefanescu',
email: 'didimitrie43@example.org',
password: 'sn3aky-1337-b1m',
id: ''
})
stream.id = await createStream({ ...stream, isPublic: false, ownerId: userOne.id })
})
+41 -74
View File
@@ -34,13 +34,7 @@ import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/se
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { createBranchFactory } from '@/modules/core/repositories/branches'
import {
getUsersFactory,
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import { getUsersFactory, getUserFactory } from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
@@ -50,7 +44,6 @@ import { requestNewEmailVerificationFactory } from '@/modules/emails/services/ve
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
finalizeInvitedServerRegistrationFactory,
@@ -75,6 +68,7 @@ import {
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import type Express from 'express'
import { createTestUser, type BasicTestUser } from '@/test/authHelper'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
const getServerInfo = getServerInfoFactory({ db })
@@ -158,33 +152,6 @@ const createStream = legacyCreateStreamFactory({
})
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const createPersonalAccessToken = createPersonalAccessTokenFactory({
storeApiToken: storeApiTokenFactory({ db }),
storeTokenScopes: storeTokenScopesFactory({ db }),
@@ -195,20 +162,10 @@ const createPersonalAccessToken = createPersonalAccessTokenFactory({
})
describe('Upload/Download Routes @api-rest', () => {
const userA = {
name: 'd1',
email: 'd.1@speckle.systems',
password: 'wowwow8charsplease',
id: '',
token: ''
}
const userB = {
name: 'd2',
email: 'd.2@speckle.systems',
password: 'wowwow8charsplease',
id: '',
token: ''
}
let userA: BasicTestUser
let tokenUserA: string
let userB: BasicTestUser
let tokenUserB: string
const testStream = {
name: 'Test Stream 01',
@@ -228,8 +185,13 @@ describe('Upload/Download Routes @api-rest', () => {
before(async () => {
;({ app } = await beforeEachContext())
userA.id = await createUser(userA)
userA.token = `Bearer ${await createPersonalAccessToken(
userA = await createTestUser({
name: 'd1',
email: 'd.1@speckle.systems',
password: 'wowwow8charsplease',
id: ''
})
tokenUserA = `Bearer ${await createPersonalAccessToken(
userA.id,
'test token user A',
[
@@ -244,8 +206,13 @@ describe('Upload/Download Routes @api-rest', () => {
]
)}`
userB.id = await createUser(userB)
userB.token = `Bearer ${await createPersonalAccessToken(
userB = await createTestUser({
name: 'd2',
email: 'd.2@speckle.systems',
password: 'wowwow8charsplease',
id: ''
})
tokenUserB = `Bearer ${await createPersonalAccessToken(
userB.id,
'test token user B',
[
@@ -289,7 +256,7 @@ describe('Upload/Download Routes @api-rest', () => {
// invalid streamId
res = await request(app)
.get(`/objects/${'thisDoesNotExist'}/null`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
expect(res).to.have.status(404)
// create some objects
@@ -297,7 +264,7 @@ describe('Upload/Download Routes @api-rest', () => {
await request(app)
.post(`/objects/${testStream.id}`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
.set('Content-type', 'multipart/form-data')
.attach('batch1', Buffer.from(JSON.stringify(objBatches[0]), 'utf8'))
.attach('batch2', Buffer.from(JSON.stringify(objBatches[1]), 'utf8'))
@@ -318,14 +285,14 @@ describe('Upload/Download Routes @api-rest', () => {
// should not allow user b to access user a's private stream
res = await request(app)
.get(`/objects/${privateTestStream.id}/${objBatches[0][0].id}`)
.set('Authorization', userB.token)
.set('Authorization', tokenUserB)
expect(res).to.have.status(401)
})
it('should not allow a non-multipart/form-data request without a boundary', async () => {
const res = await request(app)
.post(`/objects/${testStream.id}`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
.set('Content-type', 'multipart/form-data')
.send(Buffer.from(JSON.stringify(objBatches[0]), 'utf8')) //sent, not attached, so no boundary will be added to Content-type header.
expect(res).to.have.status(400)
@@ -337,7 +304,7 @@ describe('Upload/Download Routes @api-rest', () => {
it('should not allow a non-multipart/form-data request, even if it has a valid header', async () => {
const res = await request(app)
.post(`/objects/${testStream.id}`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
.set('Content-type', 'application/json')
.attach(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -354,7 +321,7 @@ describe('Upload/Download Routes @api-rest', () => {
it('should not allow non-buffered requests', async () => {
const res = await request(app)
.post(`/objects/${testStream.id}`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
.set('Content-type', 'multipart/form-data')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.attach(JSON.stringify(objBatches[0]) as any, undefined as any)
@@ -369,20 +336,20 @@ describe('Upload/Download Routes @api-rest', () => {
await request(app)
.post(`/objects/${privateTestStream.id}`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
.set('Content-type', 'multipart/form-data')
.attach('batch1', Buffer.from(JSON.stringify(objBatch), 'utf8'))
// should allow userA to access privateTestStream object
let res = await request(app)
.get(`/objects/${privateTestStream.id}/${objBatch[0].id}`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
expect(res).to.have.status(200)
// should not allow userB to access privateTestStream object by pretending it's in public stream
res = await request(app)
.get(`/objects/${testStream.id}/${objBatch[0].id}`)
.set('Authorization', userB.token)
.set('Authorization', tokenUserB)
expect(res).to.have.status(404)
})
@@ -402,7 +369,7 @@ describe('Upload/Download Routes @api-rest', () => {
// invalid streamId
res = await request(app)
.post(`/objects/${'thisDoesNotExist'}`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
expect(res).to.have.status(401)
})
@@ -420,7 +387,7 @@ describe('Upload/Download Routes @api-rest', () => {
const res = await request(app)
.post(`/objects/${testStream.id}`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
.set('Content-type', 'multipart/form-data')
.attach('batch1', Buffer.from(JSON.stringify(objectToPost), 'utf8'))
@@ -430,7 +397,7 @@ describe('Upload/Download Routes @api-rest', () => {
it('Should not allow upload with invalid body (invalid json)', async () => {
const res = await request(app)
.post(`/objects/${testStream.id}`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
.set('Content-type', 'multipart/form-data')
.attach('batch1', Buffer.from(JSON.stringify('this is not json'), 'utf8'))
@@ -445,7 +412,7 @@ describe('Upload/Download Routes @api-rest', () => {
// const res = await request(app)
// .post(`/objects/${testStream.id}`)
// .set('Authorization', userA.token)
// .set('Authorization', tokenUserA)
// .set('Content-type', 'multipart/form-data')
// .attach('batch1', Buffer.from(JSON.stringify([objectToPost]), 'utf8'))
@@ -466,7 +433,7 @@ describe('Upload/Download Routes @api-rest', () => {
const res = await request(app)
.post(`/objects/${testStream.id}`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
.set('Content-type', 'multipart/form-data')
.attach('batch1', Buffer.from(JSON.stringify(objBatches[0]), 'utf8'))
.attach('batch2', Buffer.from(JSON.stringify(objBatches[1]), 'utf8'))
@@ -486,7 +453,7 @@ describe('Upload/Download Routes @api-rest', () => {
await new Promise<void>((resolve, reject) => {
void request(app)
.get(`/objects/${testStream.id}/${parentId}`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
.buffer()
.parse((res, cb) => {
const resTyped = res as typeof res & { data: string }
@@ -518,7 +485,7 @@ describe('Upload/Download Routes @api-rest', () => {
await new Promise<void>((resolve, reject) => {
void request(app)
.get(`/objects/${testStream.id}/${parentId}`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
.set('Accept', 'text/plain')
.buffer()
.parse((res, cb) => {
@@ -557,7 +524,7 @@ describe('Upload/Download Routes @api-rest', () => {
await new Promise<void>((resolve, reject) => {
void request(app)
.post(`/api/getobjects/${testStream.id}`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
.set('Accept', 'text/plain')
.send({ objects: JSON.stringify(objectIds) })
.buffer()
@@ -594,7 +561,7 @@ describe('Upload/Download Routes @api-rest', () => {
const res = await request(app)
.post(`/api/getobjects/${testStream.id}`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
.set('Accept', 'text/plain')
.send({ objects: JSON.stringify(objectIds) })
.buffer()
@@ -605,7 +572,7 @@ describe('Upload/Download Routes @api-rest', () => {
it('Should return status code 400 when getting the list of objects and if it is not parseable', async () => {
const response = await request(app)
.post(`/api/getobjects/${testStream.id}`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
.send({ objects: ['lolz', 'thisIsBroken', 'shouldHaveBeenJSONStringified'] })
expect(response).to.have.status(400)
@@ -629,7 +596,7 @@ describe('Upload/Download Routes @api-rest', () => {
await new Promise<void>((resolve, reject) => {
void request(app)
.post(`/api/diff/${testStream.id}`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
.send({ objects: JSON.stringify(objectIds) })
.buffer()
.parse((res, cb) => {
@@ -674,7 +641,7 @@ describe('Upload/Download Routes @api-rest', () => {
it('Should return status code 400 if the list of objects is not parseable', async () => {
const response = await request(app)
.post(`/api/diff/${testStream.id}`)
.set('Authorization', userA.token)
.set('Authorization', tokenUserA)
.send({ objects: ['lolz', 'thisIsBroken', 'shouldHaveBeenJSONStringified'] })
expect(response).to.have.status(400)
+140 -51
View File
@@ -132,6 +132,14 @@ import {
import { authorizeResolver } from '@/modules/shared'
import { getUserWorkspaceSeatsFactory } from '@/modules/workspacesCore/repositories/workspaces'
import { queryAllProjectsFactory } from '@/modules/core/services/projects'
import { getTestRegionClients } from '@/modules/multiregion/tests/helpers'
import { asMultiregionalOperation } from '@/modules/shared/command'
import type {
ChangeUserPassword,
CreateValidatedUser,
DeleteUser,
UpdateUserAndNotify
} from '@/modules/core/domain/users/operations'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
const getServerInfo = getServerInfoFactory({ db })
@@ -237,63 +245,144 @@ const createStream = legacyCreateStreamFactory({
})
const grantPermissionsStream = grantStreamPermissionsFactory({ db })
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const createUser: CreateValidatedUser = async (...input) =>
asMultiregionalOperation(
async ({ mainDb, allDbs, emit }) => {
const createUser = createUserFactory({
getServerInfo,
findEmail: findEmailFactory({ db: mainDb }),
storeUser: async (...params) => {
const [user] = await Promise.all(
allDbs.map((db) => storeUserFactory({ db })(...params))
)
return user
},
countAdminUsers: countAdminUsersFactory({ db: mainDb }),
storeUserAcl: storeUserAclFactory({ db: mainDb }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db: mainDb }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({
db: mainDb
}),
findEmail: findEmailFactory({ db: mainDb }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db: mainDb }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db: mainDb })
}),
requestNewEmailVerification: requestNewEmailVerificationFactory({
getServerInfo,
findEmail: findEmailFactory({ db: mainDb }),
getUser: getUserFactory({ db: mainDb }),
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory(
{
db: mainDb
}
),
renderEmail,
sendEmail
})
}),
emitEvent: emit
})
return createUser(...input)
},
{
dbs: await getTestRegionClients(),
name: 'create user spec',
logger: dbLogger
}
)
const findOrCreateUser = findOrCreateUserFactory({
createUser,
findPrimaryEmailForUser: findPrimaryEmailForUserFactory({ db })
})
const getUserByEmail = legacyGetUserByEmailFactory({ db })
const updateUser = updateUserAndNotifyFactory({
getUser: getUserFactory({ db }),
updateUser: updateUserFactory({ db }),
emitEvent: getEventBus().emit
})
const updateUserPassword = changePasswordFactory({
getUser: getUserFactory({ db }),
updateUser: updateUserFactory({ db })
})
const updateUser: UpdateUserAndNotify = async (...input) =>
asMultiregionalOperation(
({ mainDb, allDbs, emit }) => {
const updateUserAndNotify = updateUserAndNotifyFactory({
getUser: getUserFactory({ db: mainDb }),
updateUser: async (...params) => {
const [res] = await Promise.all(
allDbs.map((db) => updateUserFactory({ db })(...params))
)
return res
},
emitEvent: emit
})
return updateUserAndNotify(...input)
},
{
logger: dbLogger,
name: 'update user and notify spec',
dbs: await getTestRegionClients()
}
)
const updateUserPassword: ChangeUserPassword = async (...input) =>
asMultiregionalOperation(
({ mainDb, allDbs }) => {
const updateUserPassword = changePasswordFactory({
getUser: getUserFactory({ db: mainDb }),
updateUser: async (...params) => {
const [res] = await Promise.all(
allDbs.map((db) => updateUserFactory({ db })(...params))
)
return res
}
})
return updateUserPassword(...input)
},
{
logger: dbLogger,
name: 'update user password spec',
dbs: await getTestRegionClients()
}
)
const validateUserPassword = validateUserPasswordFactory({
getUserByEmail: getUserByEmailFactory({ db })
})
const deleteUser = deleteUserFactory({
deleteStream: deleteStreamFactory({ db }),
logger: dbLogger,
isLastAdminUser: isLastAdminUserFactory({ db }),
getUserDeletableStreams: getUserDeletableStreamsFactory({ db }),
queryAllProjects: queryAllProjectsFactory({
getExplicitProjects: getExplicitProjects({ db })
}),
getUserWorkspaceSeats: getUserWorkspaceSeatsFactory({ db }),
deleteAllUserInvites: deleteAllUserInvitesFactory({ db }),
deleteUserRecord: deleteUserRecordFactory({ db }),
emitEvent: getEventBus().emit
})
const deleteUser: DeleteUser = async (...input) =>
asMultiregionalOperation(
({ mainDb, allDbs, emit }) => {
const deleteUser = deleteUserFactory({
deleteStream: deleteStreamFactory({ db: mainDb }),
logger: dbLogger,
isLastAdminUser: isLastAdminUserFactory({ db: mainDb }),
getUserDeletableStreams: getUserDeletableStreamsFactory({ db: mainDb }),
queryAllProjects: queryAllProjectsFactory({
getExplicitProjects: getExplicitProjects({ db: mainDb })
}),
getUserWorkspaceSeats: getUserWorkspaceSeatsFactory({ db: mainDb }),
deleteAllUserInvites: deleteAllUserInvitesFactory({ db: mainDb }),
deleteUserRecord: async (params) => {
const [res] = await Promise.all(
allDbs.map((db) => deleteUserRecordFactory({ db })(params))
)
return res
},
emitEvent: emit
})
return deleteUser(...input)
},
{
logger: dbLogger,
name: 'delete user spec',
dbs: await getTestRegionClients()
}
)
const changeUserRole = changeUserRoleFactory({
getServerInfo,
isLastAdminUser: isLastAdminUserFactory({ db }),
@@ -334,7 +423,7 @@ const createObject = createObjectFactory({
storeSingleObjectIfNotFoundFactory: storeSingleObjectIfNotFoundFactory({ db })
})
describe('Actors & Tokens @user-services', () => {
describe('Actors & Tokens @user-services @multiregion', () => {
const myTestActor = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie@example.org',
@@ -394,7 +483,7 @@ describe('Actors & Tokens @user-services', () => {
})
// Note: deletion is more complicated.
it('Should delete a user', async () => {
it('Should delete a user @multiregion', async () => {
const soloOwnerStream = {
name: 'Test Stream 01',
description: 'wonderful test stream',
@@ -47,6 +47,8 @@ import { getEventBus } from '@/modules/shared/services/eventBus'
import { expect } from 'chai'
import { getUserWorkspaceSeatsFactory } from '@/modules/workspacesCore/repositories/workspaces'
import { queryAllProjectsFactory } from '@/modules/core/services/projects'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
const getUsers = legacyGetPaginatedUsersFactory({ db })
const countUsers = legacyGetPaginatedUsersCountFactory({ db })
@@ -61,6 +63,7 @@ const requestNewEmailVerification = requestNewEmailVerificationFactory({
renderEmail,
sendEmail
})
// this does not uses createTestUser as 250 parallel transactions for user creation can timeout some of them
const createUser = createUserFactory({
getServerInfo,
findEmail,
@@ -112,8 +115,8 @@ describe('User admin @user-services', () => {
before(async () => {
await beforeEachContext()
const actorId = await createUser(myTestActor)
myTestActor.id = actorId
const actor = await createTestUser(myTestActor)
myTestActor.id = actor.id
})
it('First created user should be admin', async () => {
@@ -133,16 +136,16 @@ describe('User admin @user-services', () => {
newUser.email = 'bill@gates.com'
newUser.password = 'testthebest'
const actorId = await createUser(newUser)
const actor = await createTestUser(newUser)
expect(await countUsers()).to.equal(2)
await deleteUser(actorId)
await deleteUser(actor.id)
expect(await countUsers()).to.equal(1)
})
it('Get users query limit is sanitized to upper limit', async () => {
const userInputs = Array(250)
const userInputs: BasicTestUser[] = Array(250)
.fill(undefined)
.map((v, i) => createNewDroid(i))
@@ -191,9 +194,10 @@ describe('User admin @user-services', () => {
}
})
it('modifies role', async () => {
const userId = await createUser(
const user = await createTestUser(
createNewDroid(cryptoRandomString({ length: 13 }))
)
const userId = user.id
const oldRole = await getUserRole(userId)
expect(oldRole).to.equal(Roles.Server.User)
@@ -228,6 +232,7 @@ describe('User admin @user-services', () => {
const createNewDroid = (number: string | number) => {
return {
id: `${number}`,
name: `${number}`,
email: `${number}@droidarmy.com`,
password: 'sn3aky-1337-b1m'
@@ -35,13 +35,7 @@ import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/se
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { createBranchFactory } from '@/modules/core/repositories/branches'
import {
countAdminUsersFactory,
getUserFactory,
getUsersFactory,
storeUserAclFactory,
storeUserFactory
} from '@/modules/core/repositories/users'
import { getUserFactory, getUsersFactory } from '@/modules/core/repositories/users'
import {
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory,
@@ -51,7 +45,6 @@ import { requestNewEmailVerificationFactory } from '@/modules/emails/services/ve
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
finalizeInvitedServerRegistrationFactory,
@@ -67,6 +60,8 @@ import {
validateStreamAccessFactory
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
// To ensure that the invites are created in the correct order, we need to wait a bit between each creation
@@ -154,34 +149,6 @@ const createStream = legacyCreateStreamFactory({
})
const createInviteDirectly = createStreamInviteDirectly
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
function randomEl<T>(array: T[]): T {
return array[Math.floor(Math.random() * array.length)]
}
@@ -204,13 +171,7 @@ async function getOrderedUserIds() {
}
describe('[Admin users list]', () => {
const me = {
name: 'Mr Server Admin Dude',
email: 'adminuserguy@example.org',
password: 'sn3aky-1337-b1m',
id: undefined as Optional<string>,
verified: false
}
let me: BasicTestUser
const USER_COUNT = 15
const SERVER_INVITE_COUNT = 5
@@ -251,7 +212,13 @@ describe('[Admin users list]', () => {
await cleanup()
await createUser(me).then((id) => (me.id = id))
me = await createTestUser({
name: 'Mr Server Admin Dude',
email: 'adminuserguy@example.org',
password: 'sn3aky-1337-b1m',
id: undefined as Optional<string>,
verified: false
})
const userIds: string[] = []
let remainingSearchQueryUserCount = SEARCH_QUERY_RESULT_COUNT
@@ -260,7 +227,7 @@ describe('[Admin users list]', () => {
// Create Users
// count - 1, cause `me` also exists
for (let i = 0; i < USER_COUNT - 1; i++) {
const id = await createUser({
const { id } = await createTestUser({
name: `User #${i} - ${
remainingSearchQueryUserCount-- >= 1 ? SEARCH_QUERY : ''
}`,
@@ -1,6 +1,6 @@
import { Users } from '@/modules/core/dbSchema'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUsers } from '@/test/authHelper'
import { createTestUser, createTestUsers } from '@/test/authHelper'
import { getActiveUser, getOtherUser } from '@/test/graphql/users'
import { beforeEachContext, truncateTables } from '@/test/hooks'
import { expect } from 'chai'
@@ -32,15 +32,8 @@ import { requestNewEmailVerificationFactory } from '@/modules/emails/services/ve
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import {
countAdminUsersFactory,
getUserFactory,
storeUserAclFactory,
storeUserFactory
} from '@/modules/core/repositories/users'
import { createUserFactory } from '@/modules/core/services/users/management'
import { getUserFactory } from '@/modules/core/repositories/users'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getEventBus } from '@/modules/shared/services/eventBus'
const getServerInfo = getServerInfoFactory({ db })
const getUser = getUserFactory({ db })
@@ -64,17 +57,6 @@ const createUserEmail = validateAndCreateUserEmailFactory({
requestNewEmailVerification
})
const findEmail = findEmailFactory({ db })
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: createUserEmail,
emitEvent: getEventBus().emit
})
describe('Users (GraphQL)', () => {
const me: BasicTestUser = {
id: '',
@@ -162,7 +144,7 @@ describe('Users (GraphQL)', () => {
})
it('should return emails for user', async () => {
const userId = await createUser({
const { id: userId } = await createTestUser({
name: 'emails user',
email: createRandomEmail(),
password: createRandomPassword(),
+24 -11
View File
@@ -7,26 +7,39 @@ import {
deleteVerificationsFactory,
getPendingTokenFactory
} from '@/modules/emails/repositories'
import { db } from '@/db/knex'
import { markUserAsVerifiedFactory } from '@/modules/core/repositories/users'
import { withOperationLogging } from '@/observability/domain/businessLogging'
import { markUserEmailAsVerifiedFactory } from '@/modules/core/services/users/emailVerification'
import { updateUserEmailFactory } from '@/modules/core/repositories/userEmails'
import { getAllRegisteredDbs } from '@/modules/multiregion/utils/dbSelector'
import { asMultiregionalOperation } from '@/modules/shared/command'
export default (app: Express) => {
app.get('/auth/verifyemail', async (req, res) => {
const logger = req.log
try {
const finalizeEmailVerification = finalizeEmailVerificationFactory({
getPendingToken: getPendingTokenFactory({ db }),
markUserAsVerified: markUserAsVerifiedFactory({ db }),
deleteVerifications: deleteVerificationsFactory({ db })
})
await asMultiregionalOperation(
async ({ mainDb, allDbs }) => {
const finalizeEmailVerification = finalizeEmailVerificationFactory({
getPendingToken: getPendingTokenFactory({ db: mainDb }),
markUserAsVerified: async (params) => {
const [res] = await Promise.all(
allDbs.map((db) => markUserAsVerifiedFactory({ db })(params))
)
return res
},
deleteVerifications: deleteVerificationsFactory({ db: mainDb }),
markUserEmailAsVerified: markUserEmailAsVerifiedFactory({
updateUserEmail: updateUserEmailFactory({ db: mainDb })
})
})
await withOperationLogging(
async () => await finalizeEmailVerification(req.query.t as Optional<string>),
return await finalizeEmailVerification(req.query.t as Optional<string>)
},
{
logger,
operationName: 'finalizeEmailVerification',
operationDescription: 'Finalize email verification'
dbs: await getAllRegisteredDbs(),
name: 'finalizeEmailVerification',
description: 'Finalize email verification'
}
)
return res.redirect(
@@ -5,6 +5,7 @@ import type {
GetPendingToken
} from '@/modules/emails/domain/operations'
import type { MarkUserAsVerified } from '@/modules/core/domain/users/operations'
import type { MarkUserEmailAsVerified } from '@/modules/core/domain/userEmails/operations'
type InitializeStateDeps = {
getPendingToken: GetPendingToken
@@ -28,6 +29,7 @@ type FinalizationState = Awaited<ReturnType<ReturnType<typeof initializeState>>>
type FinalizeVerificationDeps = {
markUserAsVerified: MarkUserAsVerified
markUserEmailAsVerified: MarkUserEmailAsVerified
deleteVerifications: DeleteVerifications
}
@@ -36,7 +38,11 @@ const finalizeVerification =
const { token } = state
const { email } = token
await Promise.all([deps.markUserAsVerified(email), deps.deleteVerifications(email)])
await Promise.all([
deps.markUserEmailAsVerified({ email: email.toLowerCase().trim() }),
deps.markUserAsVerified(email),
deps.deleteVerifications(email)
])
}
/**
@@ -18,13 +18,7 @@ import {
ensureNoPrimaryEmailForUserFactory,
findEmailFactory
} from '@/modules/core/repositories/userEmails'
import {
countAdminUsersFactory,
getUserFactory,
getUsersFactory,
storeUserAclFactory,
storeUserFactory
} from '@/modules/core/repositories/users'
import { getUserFactory, getUsersFactory } from '@/modules/core/repositories/users'
import {
addOrUpdateStreamCollaboratorFactory,
validateStreamAccessFactory
@@ -35,7 +29,6 @@ import {
} from '@/modules/core/services/streams/management'
import { createTokenFactory } from '@/modules/core/services/tokens'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { createUserFactory } from '@/modules/core/services/users/management'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
@@ -81,25 +74,6 @@ export const initUploadTestEnvironment = () => {
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const createToken = createTokenFactory({
storeApiToken: storeApiTokenFactory({ db }),
storeTokenScopes: storeTokenScopesFactory({ db }),
@@ -197,7 +171,6 @@ export const initUploadTestEnvironment = () => {
return {
findEmail,
requestNewEmailVerification,
createUser,
createToken,
createStream,
getUser,
@@ -9,8 +9,9 @@ import { noErrors } from '@/test/helpers'
import { TIME_MS } from '@speckle/shared'
import { initUploadTestEnvironment } from '@/modules/fileuploads/tests/helpers/init'
import { fileURLToPath } from 'url'
import { createTestUser } from '@/test/authHelper'
const { createStream, createUser, createToken } = initUploadTestEnvironment()
const { createStream, createToken } = initUploadTestEnvironment()
const gqlQueryToListFileUploads = `query ($streamId: String!) {
stream(id: $streamId) {
id
@@ -54,7 +55,8 @@ describe('FileUploads @fileuploads integration', () => {
process.env['CANONICAL_URL'] = serverAddress
process.env['PORT'] = serverPort
userOneId = await createUser(userOne)
const user = await createTestUser(userOne)
userOneId = user.id
})
beforeEach(async () => {
createdStreamId = await createStream({ ownerId: userOneId })
@@ -267,7 +269,7 @@ describe('FileUploads @fileuploads integration', () => {
email: 'user2@example.org',
password: 'jdsadjsadasfdsa'
}
const userTwoId = await createUser(userTwo)
const { id: userTwoId } = await createTestUser(userTwo)
const streamTwoId = await createStream({ ownerId: userTwoId })
const response = await request(app)
@@ -15,8 +15,9 @@ import cryptoRandomString from 'crypto-random-string'
import type { Server } from 'http'
import { initUploadTestEnvironment } from '@/modules/fileuploads/tests/helpers/init'
import { createFileUploadJob } from '@/modules/fileuploads/tests/helpers/creation'
import { createTestUser } from '@/test/authHelper'
const { createUser, createStream, createToken } = initUploadTestEnvironment()
const { createStream, createToken } = initUploadTestEnvironment()
const { FF_NEXT_GEN_FILE_IMPORTER_ENABLED } = getFeatureFlags()
@@ -52,7 +53,8 @@ const { FF_NEXT_GEN_FILE_IMPORTER_ENABLED } = getFeatureFlags()
process.env['CANONICAL_URL'] = serverAddress
process.env['PORT'] = serverPort
userOneId = await createUser(userOne)
const user = await createTestUser(userOne)
userOneId = user.id
})
beforeEach(async () => {
@@ -200,7 +202,7 @@ const { FF_NEXT_GEN_FILE_IMPORTER_ENABLED } = getFeatureFlags()
})
it('should 403 if the token is for a different user', async () => {
const userTwoId = await createUser({
const { id: userTwoId } = await createTestUser({
name: createRandomString(),
email: createRandomEmail(),
password: createRandomPassword()
@@ -24,9 +24,9 @@ import type { JobPayload } from '@speckle/shared/workers/fileimport'
import type { EventBusEmit } from '@/modules/shared/services/eventBus'
import { FileuploadEvents } from '@/modules/fileuploads/domain/events'
import type { BranchRecord } from '@/modules/core/helpers/types'
import { createTestUser } from '@/test/authHelper'
const { createStream, createBranch, createUser, garbageCollector } =
initUploadTestEnvironment()
const { createStream, createBranch, garbageCollector } = initUploadTestEnvironment()
const { FF_NEXT_GEN_FILE_IMPORTER_ENABLED } = getFeatureFlags()
@@ -42,7 +42,8 @@ describe('FileUploads @fileuploads', () => {
let createdBranch: BranchRecord
before(async () => {
userOneId = await createUser(userOne)
const user = await createTestUser(userOne)
userOneId = user.id
})
beforeEach(async () => {
@@ -28,6 +28,7 @@ import type { MultiRegionConfig } from '@speckle/shared/environment/db'
import { getConnectionSettings } from '@speckle/shared/environment/db'
import { expect } from 'chai'
import { merge } from 'lodash-es'
import { resetRegisteredRegions } from '@/modules/multiregion/utils/dbSelector'
const isEnabled = isMultiRegionEnabled()
@@ -110,6 +111,7 @@ isEnabled
after(async () => {
setMultiRegionConfig(originalConfig)
await truncateRegionsSafely()
resetRegisteredRegions()
})
describe('server config', () => {
@@ -0,0 +1,12 @@
import { db } from '@/db/knex'
import { getRegisteredRegionClients } from '@/modules/multiregion/utils/dbSelector'
import { isMultiRegionTestMode } from '@/test/speckle-helpers/regions'
import type { Knex } from 'knex'
export async function getTestRegionClients(): Promise<[Knex, ...Knex[]]> {
if (!isMultiRegionTestMode()) return [db]
const regionClients = await getRegisteredRegionClients()
const regionDbs = Object.values(regionClients)
return [db, ...regionDbs]
}
@@ -7,23 +7,32 @@ import {
} from '@/modules/shared/helpers/dbHelper'
import { wait } from '@speckle/shared'
import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
describe('prepared transaction repository functions', () => {
describe('getStalePreparedTransactionsFactory returns a function, that', () => {
let trx: Knex
let trx: Knex.Transaction
let transactionId: string = ''
beforeEach(async () => {
trx = await db.transaction()
transactionId = await prepareTransaction(trx)
transactionId = cryptoRandomString({ length: 10 })
await prepareTransaction(trx, transactionId)
try {
await trx.commit()
} catch {}
})
afterEach(async () => {
await rollbackPreparedTransaction(trx, transactionId)
await rollbackPreparedTransaction(db, transactionId)
try {
await trx.rollback()
} catch {}
})
it('returns prepared transactions older than a given time interval', async () => {
await wait(5000)
await wait(2000)
const result = await getStalePreparedTransactionsFactory({ db })({
interval: '1 second'
})
@@ -9,7 +9,8 @@ import { getRegionFactory } from '@/modules/multiregion/repositories'
import {
DatabaseError,
LogicError,
MisconfiguredEnvironmentError
MisconfiguredEnvironmentError,
TestOnlyLogicError
} from '@/modules/shared/errors'
import { configureClient } from '@/knexfile'
import type { InitializeRegion } from '@/modules/multiregion/domain/operations'
@@ -19,8 +20,8 @@ import {
getMainRegionConfig
} from '@/modules/multiregion/regionConfig'
import type { MaybeNullOrUndefined } from '@speckle/shared'
import { ensureError } from '@speckle/shared'
import { isDevOrTestEnv, isTestEnv } from '@/modules/shared/helpers/envHelper'
import { ensureError, TIME_MS, wait } from '@speckle/shared'
import { isTestEnv } from '@/modules/shared/helpers/envHelper'
import { migrateDbToLatest } from '@/db/migrations'
import {
getProjectRegionKey,
@@ -28,6 +29,7 @@ import {
} from '@/modules/multiregion/utils/regionSelector'
import { get, mapValues } from 'lodash-es'
import { isMultiRegionEnabled } from '@/modules/multiregion/helpers'
import { logger } from '@/observability/logging'
let getter: GetProjectDb | undefined = undefined
@@ -114,13 +116,10 @@ export const initializeRegisteredRegionClients = async (): Promise<RegionClients
Object.entries(ret).map(([region, db]) => migrateDbToLatest({ db, region }))
)
// (re-)set up pub-sub, if needed
// (disabled in prod cause there's too many DBs and connections and the load is too hard to handle)
if (isDevOrTestEnv()) {
await Promise.all(
Object.keys(ret).map((regionKey) => initializeRegion({ regionKey }))
)
}
// initialize regions
await Promise.all(
Object.keys(ret).map((regionKey) => initializeRegion({ regionKey }))
)
registeredRegionClients = ret
return ret
@@ -129,6 +128,7 @@ export const initializeRegisteredRegionClients = async (): Promise<RegionClients
export const getRegisteredRegionClients = async (): Promise<RegionClients> => {
if (!registeredRegionClients)
registeredRegionClients = await initializeRegisteredRegionClients()
return registeredRegionClients
}
@@ -156,6 +156,15 @@ export const getAllRegisteredDbClients = async (): Promise<
]
}
export const getAllRegisteredDbs = async (): Promise<[Knex, ...Knex[]]> => {
const mainDb = db
const regionDbs: RegionClients = isMultiRegionEnabled()
? await getRegisteredRegionClients()
: {}
return [mainDb, ...Object.entries(regionDbs).map(([, client]) => client)]
}
/**
* Idempotently initialize region db
*/
@@ -181,7 +190,7 @@ export const initializeRegion: InitializeRegion = async ({ regionKey }) => {
? 'require'
: 'disable'
await setUpUserReplication({
await dropUserReplicationIfExists({
from: mainDb,
to: regionDb,
regionName: regionKey,
@@ -210,104 +219,63 @@ interface ReplicationArgs {
regionName: string
}
const setUpUserReplication = async ({
const dropUserReplicationIfExists = async ({
from,
to,
sslmode,
regionName
}: ReplicationArgs): Promise<void> => {
const subName = createPubSubName(`userssub_${regionName}`)
const pubName = createPubSubName('userspub')
try {
await from.public.raw(`CREATE PUBLICATION ${pubName} FOR TABLE users;`)
} catch (err) {
if (!(err instanceof Error)) {
throw new DatabaseError(
'Could not create publication {pubName} when setting up user replication for region {regionName}',
from.public,
{
cause: ensureError(
sanitizeError(err),
'Unknown database error when creating publication'
),
info: { pubName, regionName }
}
)
const { rows: pubExist } = await from.public.raw(
`SELECT pubname FROM pg_publication WHERE pubname = '${pubName}';`
)
if (pubExist.length > 0) {
await from.public.raw(`DROP PUBLICATION ${pubName};`)
logger.info({ regionName, pubName }, 'dropped publication')
}
const errorMessage = err.message
if (
!['already exists', 'violates unique constraint'].some((message) =>
errorMessage.includes(message)
)
)
throw new DatabaseError(
'Unknown error while creating publication {pubName} when setting up user replication for region {regionName}',
from.public,
{
cause: ensureError(
sanitizeError(err),
'Unknown database error when creating publication'
),
info: { pubName, regionName }
}
)
} catch (error) {
logger.warn({ error }, 'while dropping publication')
// silent error as
// dropping pub can have race conditions (n subs - 1 pub)
// and action DROP PUBLICATION does not support if exist for current postgres version
}
const fromUrl = new URL(
from.private
? from.private.client.config.connection.connectionString
: from.public.client.config.connection.connectionString
)
const port = fromUrl.port ? fromUrl.port : '5432'
const fromDbName = fromUrl.pathname.replace('/', '')
const rawSqeel = `SELECT * FROM aiven_extras.pg_create_subscription(
?,
?,
?,
?,
TRUE,
TRUE
);`
try {
await to.public.raw('CREATE EXTENSION IF NOT EXISTS "aiven_extras"')
await to.public.raw(rawSqeel, [
subName,
`dbname=${fromDbName} host=${fromUrl.hostname} port=${port} sslmode=${sslmode} user=${fromUrl.username} password=${fromUrl.password}`,
pubName,
subName
])
} catch (err) {
if (!(err instanceof Error))
throw new DatabaseError(
'Could not create subscription {subName} to {pubName} when setting up user replication for region {regionName}',
to.public,
{
cause: ensureError(
sanitizeError(err),
'Unknown database error when creating subscription'
),
info: { subName, pubName, regionName }
}
)
if (
!err.message.includes('already exists') &&
!err.message.includes('duplicate key value violates unique constraint')
const { rows: aivenExists } = await to.public.raw(
"SELECT * FROM pg_extension WHERE extname = 'aiven_extras';"
)
throw new DatabaseError(
'Unknown error while creating subscription {subName} to {pubName} when setting up user replication for region {regionName}',
to.public,
{
cause: ensureError(
sanitizeError(err),
'Unknown database error when creating subscription'
),
info: { subName, pubName, regionName }
}
)
if (!aivenExists) return
const {
rows: [sub]
} = await to.public.raw<{ rows: { subconninfo: string; subslotname: string }[] }>(
`SELECT subconninfo, subslotname FROM aiven_extras.pg_list_all_subscriptions() WHERE subname = '${subName}';`
)
if (!sub) return
await to.public.raw(
`SELECT * FROM aiven_extras.pg_alter_subscription_disable('${subName}');`
)
await wait(TIME_MS.second)
await to.public.raw(
`SELECT * FROM aiven_extras.pg_drop_subscription('${subName}');`
)
await wait(TIME_MS.second)
await to.public.raw(
`SELECT * FROM aiven_extras.dblink_slot_create_or_drop('${sub.subconninfo}', '${sub.subslotname}', 'drop');`
)
logger.info({ regionName, subName }, 'dropped subscription')
} catch (error) {
logger.error({ error }, 'Failed to drop subscription')
return
}
return
}
const setUpProjectReplication = async ({
@@ -410,3 +378,11 @@ const sanitizeError = (err: unknown): unknown => {
if ((get(err, 'where') as unknown as string).includes('password='))
return { ...err, where: '[REDACTED AS IT CONTAINS CONNECTION STRING]' }
}
export const resetRegisteredRegions = () => {
if (!isTestEnv()) {
throw new TestOnlyLogicError()
}
registeredRegionClients = undefined
}
+26 -15
View File
@@ -9,6 +9,7 @@ import {
import { changePasswordFactory } from '@/modules/core/services/users/management'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { getAllRegisteredDbs } from '@/modules/multiregion/utils/dbSelector'
import {
createTokenFactory,
deleteTokensFactory,
@@ -16,6 +17,7 @@ import {
} from '@/modules/pwdreset/repositories'
import { finalizePasswordResetFactory } from '@/modules/pwdreset/services/finalize'
import { requestPasswordRecoveryFactory } from '@/modules/pwdreset/services/request'
import { asMultiregionalOperation } from '@/modules/shared/command'
import { BadRequestError } from '@/modules/shared/errors'
import { withOperationLogging } from '@/observability/domain/businessLogging'
import { ensureError } from '@speckle/shared'
@@ -55,25 +57,34 @@ export default function (app: Express) {
app.post('/auth/pwdreset/finalize', async (req, res) => {
const logger = req.log
try {
const finalizePasswordReset = finalizePasswordResetFactory({
getUserByEmail,
getPendingToken: getPendingTokenFactory({ db }),
deleteTokens: deleteTokensFactory({ db }),
updateUserPassword: changePasswordFactory({
getUser: getUserFactory({ db }),
updateUser: updateUserFactory({ db })
}),
deleteExistingAuthTokens: deleteExistingAuthTokensFactory({ db })
})
if (!req.body.tokenId || !req.body.password)
throw new BadRequestError('Invalid request.')
await withOperationLogging(
async () => await finalizePasswordReset(req.body.tokenId, req.body.password),
await asMultiregionalOperation(
async ({ mainDb, allDbs }) => {
const finalizePasswordReset = finalizePasswordResetFactory({
getUserByEmail,
getPendingToken: getPendingTokenFactory({ db: mainDb }),
deleteTokens: deleteTokensFactory({ db: mainDb }),
updateUserPassword: changePasswordFactory({
getUser: getUserFactory({ db: mainDb }),
updateUser: async (...params) => {
const [res] = await Promise.all(
allDbs.map((db) => updateUserFactory({ db })(...params))
)
return res
}
}),
deleteExistingAuthTokens: deleteExistingAuthTokensFactory({ db: mainDb })
})
return await finalizePasswordReset(req.body.tokenId, req.body.password)
},
{
logger,
operationName: 'finalizePasswordReset',
operationDescription: `Finalizing password reset`
dbs: await getAllRegisteredDbs(),
name: 'finalizePasswordReset',
description: `Finalizing password reset`
}
)
@@ -6,62 +6,9 @@ import { beforeEachContext } from '@/test/hooks'
import { localAuthRestApi } from '@/modules/auth/tests/helpers/registration'
import { expectToThrow } from '@/test/assertionHelper'
import { expect } from 'chai'
import {
findEmailFactory,
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import {
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getEventBus } from '@/modules/shared/services/eventBus'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
const ResetTokens = () => knex('pwdreset_tokens')
const db = knex
const getServerInfo = getServerInfoFactory({ db })
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
describe('Password reset requests @passwordresets', () => {
let app: Awaited<ReturnType<typeof beforeEachContext>>['app']
@@ -71,13 +18,12 @@ describe('Password reset requests @passwordresets', () => {
})
it('Should carefully send a password request email', async () => {
const userA: BasicTestUser = {
const userA = await createTestUser({
name: 'd1',
email: 'd@speckle.systems',
password: 'wowwow8charsplease',
id: ''
}
userA.id = await createUser(userA)
})
// invalid request
await request(app).post('/auth/pwdreset/request').expect(400)
@@ -102,13 +48,12 @@ describe('Password reset requests @passwordresets', () => {
})
it('Should reset passwords', async () => {
const userB: BasicTestUser = {
const userB = await createTestUser({
name: 'd2',
email: 'd2@speckle.systems',
password: 'w0ww0w8charsplease',
id: ''
}
userB.id = await createUser(userB)
})
const authRestApi = localAuthRestApi({ express: app })
const newPassword = '12345678'
+148 -1
View File
@@ -1,5 +1,10 @@
import { mainDb } from '@/db/knex'
import { withTransaction } from '@/modules/shared/helpers/dbHelper'
import {
commitPreparedTransaction as commitPrepared,
prepareTransaction,
rollbackPreparedTransaction as rollbackPrepared,
withTransaction
} from '@/modules/shared/helpers/dbHelper'
import type {
EmitArg,
EventBus,
@@ -8,9 +13,12 @@ import type {
import { getEventBus } from '@/modules/shared/services/eventBus'
import { withOperationLogging } from '@/observability/domain/businessLogging'
import type { MaybeAsync } from '@speckle/shared'
import cryptoRandomString from 'crypto-random-string'
import type { Knex } from 'knex'
import { isBoolean } from 'lodash-es'
import type { Logger } from 'pino'
import { wasRejected } from '@/modules/shared/domain/constants'
import { RegionalTransactionFatalError } from '@/modules/shared/errors'
/**
* @deprecated asOperation does this and more. Also many usages of commandFactory are broken
@@ -120,3 +128,142 @@ export const asOperation = async <T>(
}
)
}
/**
* Utility function to execute a command across multiple regions
* works similarly to asOperation, but provides references to every db instance in the dbs array provided
* It opens a transaction for each db, and uses 2PC to ensure consistency at commit moment
* txs represents all the transactions
* dbTx represents the main transaction (Knex)
* regionTxs represents the transactions that were given as regions (Knex[])
*/
export const asMultiregionalOperation = async <T, K extends [Knex, ...Knex[]]>(
operation: (args: {
/**
* @description reference to all dbs involved in the operation
*/
allDbs: Knex[]
/**
* @description reference to the main db (first one passed in the array)
*/
mainDb: Knex
/**
* @description reference for second db (first one not main)
*/
regionDb: Knex
/**
* @description reference for all regions (all dbs except the main one)
*/
regionDbs: Knex[]
emit: EventBusEmit
}) => MaybeAsync<T>,
params: {
name: string
logger: Logger
description?: string
/**
* @description Dbs to open transactions for the operation
*/
dbs: K
/**
* @description Defaults to main event bus
*/
eventBus?: EventBus
}
): Promise<T> => {
const {
eventBus = getEventBus(),
logger,
name,
description,
dbs: [mainDb, ...regionDbs]
} = params
return await withOperationLogging(
async () => {
const events: EmitArg[] = []
const emit: EventBusEmit = async ({ eventName, payload }) => {
events.push({ eventName, payload })
}
const gid = cryptoRandomString({ length: 10 })
const trxs: Knex.Transaction[] = []
const rollback = async () => {
await Promise.allSettled(trxs.map((trx) => rollbackPrepared(trx, gid)))
await Promise.allSettled(trxs.map((trx) => trx.rollback()))
}
let result
try {
const mainDbTx = await mainDb.transaction()
trxs.push(mainDbTx)
const regionDbsTx: Knex.Transaction[] = []
for (const regionDb of regionDbs) {
const regionTx = await regionDb.transaction()
trxs.push(regionTx)
regionDbsTx.push(regionTx)
}
result = await operation({
mainDb: mainDbTx,
allDbs: trxs,
regionDb: regionDbsTx[0],
regionDbs: regionDbsTx,
emit
})
// Every transaction is prepared
// - important to do prepare sequentially
// - if a query won't complete, every preparedTransaction is rollbacked (from prepared or unprepared)
// - this applies a lock on the rows to be updated to assure that the commit will succeed.
// - the transactions once prepared, gets written to disk db and is no longer scoped to the connection.
// - this last part knex does not handle well, so no matter what, we need to rollback/commit
// the transaction (the prepared one and the connection transaction) that's why it's wrapped in a transaction block
for (const tx of trxs) await prepareTransaction(tx, gid)
} catch (e) {
await rollback()
throw e
}
const commits = await Promise.allSettled(
trxs.map(async (trx) => {
await commitPrepared(trx, gid)
try {
await trx.commit()
} catch {
// forcing knex to release connection
// for the db this tx is gone already as its in a prepared state unbinded from the connection
// but knex does not know this, and it won't release the connection until a commit/rollback happen
}
})
)
if (commits.some(wasRejected)) {
// we never should reach this point
// as once a transaction is prepared successfully
// it will commit
logger.error(
{ commits, gid },
`Failed to commit transactions in 2PC operation.`
)
throw new RegionalTransactionFatalError(
'Failed some or all transactions in 2PC operation.',
{ clients: trxs, gid }
)
}
for (const event of events) await eventBus.emit(event)
return result
},
{
logger,
operationName: name,
operationDescription: description
}
)
}
@@ -1,3 +1,8 @@
import { StringEnum } from '@speckle/shared'
export const PromiseAllSettledResultStatus = StringEnum(['rejected', 'fulfilled'])
export const wasRejected = <T>(
result: PromiseSettledResult<T>
): result is PromiseRejectedResult =>
result.status === PromiseAllSettledResultStatus.rejected
+4 -30
View File
@@ -163,33 +163,6 @@ export class TestOnlyLogicError extends BaseError {
static statusCode = 500
}
const getErrorInfoFromTransactions = (
preparedTransactions: { knex: Knex; preparedId: string }[]
) => {
return preparedTransactions.map(({ knex, preparedId }) => ({
db: retrieveMetadataFromDatabaseClient(knex),
gid: preparedId
}))
}
// 2PC failed but we successfully rolled back all prepared transactions.
export class RegionalTransactionError extends BaseError {
static code = 'REGIONAL_TRANSACTION_ERROR'
static defaultMessage = 'Failed to complete 2PC operation'
static statusCode = 500
constructor(
message?: string | null,
preparedTransactions: { knex: Knex; preparedId: string }[] = []
) {
super(message, {
info: {
clients: getErrorInfoFromTransactions(preparedTransactions)
}
})
}
}
// 2PC failed and we failed to rollback. A prepared transaction may have been left behind.
export class RegionalTransactionFatalError extends BaseError {
static code = 'REGIONAL_TRANSACTION_FATAL_ERROR'
@@ -197,12 +170,13 @@ export class RegionalTransactionFatalError extends BaseError {
static statusCode = 500
constructor(
message?: string | null,
preparedTransactions: { knex: Knex; preparedId: string }[] = []
message: string | null,
{ gid, clients }: { gid: string; clients: Knex[] }
) {
super(message, {
info: {
clients: getErrorInfoFromTransactions(preparedTransactions)
gid,
dbs: clients.map(retrieveMetadataFromDatabaseClient)
}
})
}
@@ -1,12 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Knex } from 'knex'
import { postgresMaxConnections } from '@/modules/shared/helpers/envHelper'
import {
EnvironmentResourceError,
LogicError,
RegionalTransactionFatalError,
RegionalTransactionError
} from '@/modules/shared/errors'
import { EnvironmentResourceError } from '@/modules/shared/errors'
import type { MaybeAsync } from '@speckle/shared'
import { isNonNullable } from '@speckle/shared'
import { base64Decode, base64Encode } from '@/modules/shared/helpers/cryptoHelper'
@@ -15,10 +10,6 @@ import dayjs from 'dayjs'
import type { MaybeNullOrUndefined, Nullable } from '@speckle/shared'
import type { SchemaConfig } from '@/modules/core/dbSchema'
import { has, isObjectLike, isString, mapValues, pick, times } from 'lodash-es'
import cryptoRandomString from 'crypto-random-string'
import { logger } from '@/observability/logging'
import { isEqual } from 'lodash-es'
import { PromiseAllSettledResultStatus } from '@/modules/shared/domain/constants'
export type Collection<T> = {
cursor: string | null
@@ -312,16 +303,8 @@ export const compositeCursorTools = <
}
}
export const prepareTransaction = async (db: Knex): Promise<string> => {
if (!db.isTransaction) {
throw new LogicError('Cannot PREPARE postgres operation outside of a transaction')
}
const preparedId = cryptoRandomString({ length: 10 })
await db.raw(`PREPARE TRANSACTION '${preparedId}';`)
return preparedId
export const prepareTransaction = async (db: Knex, gid: string): Promise<void> => {
await db.raw(`PREPARE TRANSACTION '${gid}';`)
}
export const commitPreparedTransaction = async (
@@ -337,102 +320,3 @@ export const rollbackPreparedTransaction = async (
): Promise<void> => {
await db.raw(`ROLLBACK PREPARED '${gid}';`)
}
export const replicateQuery = <T, U>(
dbs: Knex[],
factory: ({ db }: { db: Knex }) => (params: T) => Promise<U>
) => {
return async (params: T) => {
const preparedTransactions: {
knex: Knex
preparedId: string
}[] = []
const returnValues: U[] = []
try {
// Phase 1: Prepare transaction across all specified db instances
for (const db of dbs) {
const trx = await db.transaction()
const returnValue = await factory({ db: trx })(params)
returnValues.push(returnValue)
const preparedId = await prepareTransaction(trx)
preparedTransactions.push({ knex: db, preparedId })
}
// Phase 2: Attempt commit of all prepared transactions
const results = await Promise.allSettled(
preparedTransactions.map(({ knex, preparedId }) => {
return commitPreparedTransaction(knex, preparedId)
})
)
const errors = results.filter((result): result is PromiseRejectedResult => {
return result.status === PromiseAllSettledResultStatus.rejected
})
if (errors.length > 0) {
logger.error(
{
params,
errors,
errorCount: errors.length,
resultCount: results.length
},
`Failed {errorCount} of {resultCount} transactions in 2PC operation.`
)
throw new RegionalTransactionError(
'Failed some or all transactions in 2PC operation.',
preparedTransactions
)
}
// TODO: Do we need this validation?
if (!returnValues.every((value) => isEqual(value, returnValues[0]))) {
throw new RegionalTransactionError(
'Return values of 2PC transactions do not match',
preparedTransactions
)
}
return returnValues[0]
} catch {
const rollbacks = preparedTransactions.map(async ({ knex, preparedId }) => {
try {
await rollbackPreparedTransaction(knex, preparedId)
} catch (err) {
logger.error(
{ preparedId },
'Failed to rollback prepared transaction {preparedId}'
)
throw err
}
})
logger.warn(
{
preparedTransactions: preparedTransactions.map(({ preparedId }) => preparedId)
},
'Error during 2PC operation. Rolling back all transactions.'
)
const results = await Promise.allSettled(rollbacks)
if (
results.some(
(result) => result.status === PromiseAllSettledResultStatus.rejected
)
) {
throw new RegionalTransactionFatalError(
'Failed to rollback all transactions.',
preparedTransactions
)
}
throw new RegionalTransactionError(
'Failed to complete 2PC operation but successfully recovered.',
preparedTransactions
)
}
}
}
@@ -2,15 +2,18 @@ import { getDb } from '@/modules/multiregion/utils/dbSelector'
import { Scopes } from '@/modules/core/dbSchema'
import { expect } from 'chai'
import type { Knex } from 'knex'
import { replicateQuery } from '@/modules/shared/helpers/dbHelper'
import { isMultiRegionTestMode } from '@/test/speckle-helpers/regions'
import { db } from '@/db/knex'
import { sleep } from '@/test/helpers'
import { asMultiregionalOperation } from '@/modules/shared/command'
import { logger } from '@/observability/logging'
isMultiRegionTestMode()
? describe('Prepared transaction utils (2PC) @multiregion', async () => {
let main: Knex
let region1: Knex
let region2: Knex
let ALL_DBS: Knex[] = []
let ALL_DBS: [Knex, ...Knex[]] = [db]
const testOperationFactory =
({ db }: { db: Knex }) =>
@@ -23,12 +26,16 @@ isMultiRegionTestMode()
}
before(async () => {
main = await getDb({ regionKey: null })
main = db
region1 = await getDb({ regionKey: 'region1' })
region2 = await getDb({ regionKey: 'region2' })
ALL_DBS = [main, region1, region2]
})
beforeEach(async () => {
await db('users').del()
})
it('successfully replicates operation across all specified db instances', async () => {
const testOperationParams = {
name: 'test:scope:a',
@@ -36,7 +43,17 @@ isMultiRegionTestMode()
public: false
}
await replicateQuery(ALL_DBS, testOperationFactory)(testOperationParams)
await asMultiregionalOperation(
({ allDbs }) =>
Promise.all(
allDbs.map((db) => testOperationFactory({ db })(testOperationParams))
),
{
dbs: ALL_DBS,
name: 'testing regional success',
logger
}
)
const scopeMain = await main
.table(Scopes.name)
@@ -66,11 +83,17 @@ isMultiRegionTestMode()
await testOperationFactory({ db: region2 })(testOperationParams)
const promise = replicateQuery(
ALL_DBS,
testOperationFactory
)(testOperationParams)
const promise = asMultiregionalOperation(
({ allDbs }) =>
Promise.all(
allDbs.map((db) => testOperationFactory({ db })(testOperationParams))
),
{
dbs: ALL_DBS,
name: 'testing regional failure',
logger
}
)
await expect(promise).eventually.to.be.rejected
const scopeMain = await main
@@ -99,16 +122,20 @@ isMultiRegionTestMode()
}
const dbThatFails = {
transaction: () =>
Promise.resolve(() => ({
insert: () => Promise.resolve()
})) // will fail on raw call
transaction: async () => Promise.reject(new Error('Transaction failed'))
} as unknown as Knex
const promise = replicateQuery(
[...ALL_DBS, dbThatFails],
testOperationFactory
)(testOperationParams)
const promise = asMultiregionalOperation(
({ allDbs }) =>
Promise.all(
allDbs.map((db) => testOperationFactory({ db })(testOperationParams))
),
{
dbs: [...ALL_DBS, dbThatFails],
name: 'testing regional success',
logger
}
)
await expect(promise).to.eventually.be.rejected
@@ -129,5 +156,32 @@ isMultiRegionTestMode()
expect(scopeRegion1).to.be.undefined
expect(scopeRegion2).to.be.undefined
})
it('does not has visibile perfomance issues using 2PC', async () => {
const connectionsUsedBefore = main.client.pool.numUsed()
const oneKnexInstanceCall = async () => {
const { buildBasicTestUser, createTestUser } = await import(
'@/test/authHelper'
)
const user = buildBasicTestUser()
await createTestUser(user) // This uses the asMultireagionOperation helper }
}
const manyParallelCreates = async () => {
await Promise.allSettled(Array.from({ length: 1000 }, oneKnexInstanceCall))
}
await manyParallelCreates()
const [{ count }] = await db('users').count()
expect(count).to.eql(1000)
await sleep(1000) // just in case
const connectionsUsedAfter = main.client.pool.numUsed()
expect(connectionsUsedAfter).to.be.lte(connectionsUsedBefore)
})
})
: null
@@ -52,13 +52,7 @@ import {
import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection'
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import {
countAdminUsersFactory,
getUserFactory,
getUsersFactory,
storeUserAclFactory,
storeUserFactory
} from '@/modules/core/repositories/users'
import { getUserFactory, getUsersFactory } from '@/modules/core/repositories/users'
import {
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory,
@@ -68,7 +62,6 @@ import { requestNewEmailVerificationFactory } from '@/modules/emails/services/ve
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
finalizeInvitedServerRegistrationFactory,
@@ -92,6 +85,8 @@ import {
validateStreamAccessFactory
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
const getServerInfo = getServerInfoFactory({ db })
@@ -191,33 +186,6 @@ const createStream = legacyCreateStreamFactory({
emitEvent: getEventBus().emit
})
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const createPersonalAccessToken = createPersonalAccessTokenFactory({
storeApiToken: storeApiTokenFactory({ db }),
storeTokenScopes: storeTokenScopesFactory({ db }),
@@ -253,23 +221,8 @@ describe('Server stats services @stats-services', function () {
describe('Server stats api @stats-api', function () {
let sendRequest: Awaited<ReturnType<typeof initializeTestServer>>['sendRequest']
const adminUser = {
name: 'Dimitrie',
password: 'TestPasswordSecure',
email: 'spam@spam.spam',
id: '', // Will be filled in before()
goodToken: '',
badToken: ''
}
const notAdminUser = {
name: 'Andrei',
password: 'TestPasswordSecure',
email: 'spasm@spam.spam',
id: '', // Will be filled in before()
goodToken: '',
badToken: ''
}
let adminUser: BasicTestUser & { goodToken?: string; badToken?: string }
let notAdminUser: BasicTestUser & { goodToken?: string; badToken?: string }
const fullQuery = `
query{
@@ -291,7 +244,12 @@ describe('Server stats api @stats-api', function () {
const ctx = await beforeEachContext()
;({ sendRequest } = await initializeTestServer(ctx))
adminUser.id = await createUser(adminUser)
adminUser = await createTestUser({
name: 'Dimitrie',
password: 'TestPasswordSecure',
email: 'spam@spam.spam',
id: ''
})
adminUser.goodToken = `Bearer ${await createPersonalAccessToken(
adminUser.id,
'test token user A',
@@ -303,7 +261,12 @@ describe('Server stats api @stats-api', function () {
[Scopes.Streams.Read]
)}`
notAdminUser.id = await createUser(notAdminUser)
notAdminUser = await createTestUser({
name: 'Andrei',
password: 'TestPasswordSecure',
email: 'spasm@spam.spam',
id: ''
})
notAdminUser.goodToken = `Bearer ${await createPersonalAccessToken(
notAdminUser.id,
'test token user A',
@@ -369,22 +332,20 @@ async function seedDb({
numCommits = 10
} = {}) {
// create users
const userPromises = []
const users = []
for (let i = 0; i < numUsers; i++) {
const promise = createUser({
const user = await createTestUser({
name: `User ${i}`,
password: `SuperSecure${i}${i * 3.14}`,
email: `user${i}@speckle.systems`
})
userPromises.push(promise)
users.push(user)
}
const userIds = await Promise.all(userPromises)
// create streams
const streamPromises: Array<Promise<{ id: string; ownerId: string }>> = []
for (let i = 0; i < numStreams; i++) {
const ownerId = userIds[i >= userIds.length ? userIds.length - 1 : i]
const { id: ownerId } = users[i >= users.length ? users.length - 1 : i]
const promise = createStream({
name: `Stream ${i}`,
ownerId
@@ -16,19 +16,12 @@ import {
ensureNoPrimaryEmailForUserFactory,
findEmailFactory
} from '@/modules/core/repositories/userEmails'
import {
countAdminUsersFactory,
getUserFactory,
getUsersFactory,
storeUserAclFactory,
storeUserFactory
} from '@/modules/core/repositories/users'
import { getUserFactory, getUsersFactory } from '@/modules/core/repositories/users'
import {
createStreamReturnRecordFactory,
legacyCreateStreamFactory
} from '@/modules/core/services/streams/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { createUserFactory } from '@/modules/core/services/users/management'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
@@ -63,6 +56,7 @@ import {
validateStreamAccessFactory
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import { createTestUser } from '@/test/authHelper'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
const WEBHOOKS_CONFIG_TABLE = 'webhooks_config'
@@ -152,33 +146,6 @@ const createStream = legacyCreateStreamFactory({
emitEvent: getEventBus().emit
})
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const countWebhooks = async () => {
const [{ count }] = await WebhooksConfig().count()
@@ -208,7 +175,7 @@ describe('Webhooks cleanup @webhooks', () => {
})
it('Cleans orphans, leaves live ones intact', async () => {
const ownerId = await createUser({
const { id: ownerId } = await createTestUser({
name: 'User',
email: createRandomEmail(),
password: createRandomPassword()
@@ -46,13 +46,7 @@ import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/se
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { createBranchFactory } from '@/modules/core/repositories/branches'
import {
getUserFactory,
getUsersFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import { getUserFactory, getUsersFactory } from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
@@ -62,7 +56,6 @@ import { requestNewEmailVerificationFactory } from '@/modules/emails/services/ve
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
finalizeInvitedServerRegistrationFactory,
@@ -86,6 +79,7 @@ import {
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import { omit } from 'lodash-es'
import { createTestUser, type BasicTestUser } from '@/test/authHelper'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
const getServerInfo = getServerInfoFactory({ db })
@@ -173,33 +167,6 @@ const createStream = legacyCreateStreamFactory({
})
})
const grantPermissionsStream = grantStreamPermissionsFactory({ db })
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const createPersonalAccessToken = createPersonalAccessTokenFactory({
storeApiToken: storeApiTokenFactory({ db }),
storeTokenScopes: storeTokenScopesFactory({ db }),
@@ -213,13 +180,7 @@ describe('Webhooks @webhooks', () => {
const getWebhook = getWebhookByIdFactory({ db })
let sendRequest: Awaited<ReturnType<typeof initializeTestServer>>['sendRequest']
const userOne = {
name: 'User',
email: 'user@example.org',
password: 'jdsadjsadasfdsa',
id: '',
token: ''
}
let userOne: BasicTestUser & { token?: string }
const streamOne = {
name: 'streamOne',
@@ -243,7 +204,12 @@ describe('Webhooks @webhooks', () => {
const ctx = await beforeEachContext()
;({ sendRequest } = await initializeTestServer(ctx))
userOne.id = await createUser(userOne)
userOne = await createTestUser({
name: 'User',
email: 'user@example.org',
password: 'jdsadjsadasfdsa',
id: ''
})
streamOne.ownerId = userOne.id
streamOne.id = await createStream(streamOne)
@@ -380,13 +346,7 @@ describe('Webhooks @webhooks', () => {
})
describe('GraphQL API Webhooks @webhooks-api', () => {
const userTwo = {
name: 'User2',
email: 'user2@example.org',
password: 'jdsadjsadasfdsa',
id: '',
token: ''
}
let userTwo: BasicTestUser & { token?: string }
const webhookTwo = {
streamId: '',
@@ -407,7 +367,12 @@ describe('Webhooks @webhooks', () => {
}
before(async () => {
userTwo.id = await createUser(userTwo)
userTwo = await createTestUser({
name: 'User2',
email: 'user2@example.org',
password: 'jdsadjsadasfdsa',
id: ''
})
streamTwo.ownerId = userTwo.id
streamTwo.id = await createStream(streamTwo)
webhookTwo.streamId = streamTwo.id
+54 -43
View File
@@ -57,7 +57,6 @@ import {
findVerifiedEmailsByUserIdFactory,
updateUserEmailFactory
} from '@/modules/core/repositories/userEmails'
import { withTransaction } from '@/modules/shared/helpers/dbHelper'
import type { UserWithOptionalRole } from '@/modules/core/repositories/users'
import {
countAdminUsersFactory,
@@ -142,6 +141,8 @@ import {
createWorkspaceSeatFactory,
getWorkspaceUserSeatFactory
} from '@/modules/gatekeeper/repositories/workspaceSeat'
import { getAllRegisteredDbs } from '@/modules/multiregion/utils/dbSelector'
import { asMultiregionalOperation } from '@/modules/shared/command'
const moveAuthParamsToSessionMiddleware = moveAuthParamsToSessionMiddlewareFactory()
const sessionMiddleware = sessionMiddlewareFactory()
@@ -275,11 +276,11 @@ export const getSsoRouter = (): Router => {
}),
async (req, res, next) => {
try {
await withTransaction(
async ({ db: trx }) => {
await asMultiregionalOperation(
async ({ mainDb, allDbs }) => {
const handleOidcCallback = handleOidcCallbackFactory({
getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }),
getWorkspaceBySlug: getWorkspaceBySlugFactory({ db: trx }),
getWorkspaceRoles: getWorkspaceRolesFactory({ db: mainDb }),
getWorkspaceBySlug: getWorkspaceBySlugFactory({ db: mainDb }),
createOidcProvider: createOidcProviderFactory({
getOIDCProviderValidationRequest:
getOIDCProviderValidationRequestFactory({
@@ -288,60 +289,66 @@ export const getSsoRouter = (): Router => {
}),
saveSsoProviderRegistration: saveSsoProviderRegistrationFactory({
getWorkspaceSsoProvider: getWorkspaceSsoProviderFactory({
db: trx,
db: mainDb,
decrypt: getDecryptor()
}),
storeProviderRecord: storeSsoProviderRecordFactory({
db: trx,
db: mainDb,
encrypt: getEncryptor()
}),
associateSsoProviderWithWorkspace:
associateSsoProviderWithWorkspaceFactory({
db: trx
db: mainDb
})
})
}),
getOidcProvider: getOidcProviderFactory({
getWorkspaceSsoProvider: getWorkspaceSsoProviderFactory({
db: trx,
db: mainDb,
decrypt: getDecryptor()
})
}),
getOidcProviderUserData: getOidcProviderUserDataFactory(),
tryGetSpeckleUserData: tryGetSpeckleUserDataFactory({
findEmail: findEmailFactory({ db: trx }),
getUser: getUserFactory({ db: trx }),
getUserEmails: findEmailsByUserIdFactory({ db: trx })
findEmail: findEmailFactory({ db: mainDb }),
getUser: getUserFactory({ db: mainDb }),
getUserEmails: findEmailsByUserIdFactory({ db: mainDb })
}),
createWorkspaceUserFromSsoProfile:
createWorkspaceUserFromSsoProfileFactory({
createUser: createUserFactory({
getServerInfo: getServerInfoFactory({ db: trx }),
findEmail: findEmailFactory({ db: trx }),
storeUser: storeUserFactory({ db: trx }),
countAdminUsers: countAdminUsersFactory({ db: trx }),
storeUserAcl: storeUserAclFactory({ db: trx }),
getServerInfo: getServerInfoFactory({ db: mainDb }),
findEmail: findEmailFactory({ db: mainDb }),
storeUser: async (...params) => {
const [user] = await Promise.all(
allDbs.map((db) => storeUserFactory({ db })(...params))
)
return user
},
countAdminUsers: countAdminUsersFactory({ db: mainDb }),
storeUserAcl: storeUserAclFactory({ db: mainDb }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db: trx }),
createUserEmail: createUserEmailFactory({ db: mainDb }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({
db: trx
db: mainDb
}),
findEmail: findEmailFactory({ db: trx }),
findEmail: findEmailFactory({ db: mainDb }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({
db: trx
db: mainDb
}),
updateAllInviteTargets: updateAllInviteTargetsFactory({
db: trx
db: mainDb
})
}),
requestNewEmailVerification: requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db: trx }),
getUser: getUserFactory({ db: trx }),
getServerInfo: getServerInfoFactory({ db: trx }),
findEmail: findEmailFactory({ db: mainDb }),
getUser: getUserFactory({ db: mainDb }),
getServerInfo: getServerInfoFactory({ db: mainDb }),
deleteOldAndInsertNewVerification:
deleteOldAndInsertNewVerificationFactory({
db: trx
db: mainDb
}),
renderEmail,
sendEmail
@@ -351,46 +358,50 @@ export const getSsoRouter = (): Router => {
}),
addOrUpdateWorkspaceRole: addOrUpdateWorkspaceRoleFactory({
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({
db: trx
db: mainDb
}),
findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({
db: trx
db: mainDb
}),
getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }),
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db: trx }),
getWorkspaceRoles: getWorkspaceRolesFactory({ db: mainDb }),
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db: mainDb }),
emitWorkspaceEvent: getEventBus().emit,
ensureValidWorkspaceRoleSeat: ensureValidWorkspaceRoleSeatFactory({
createWorkspaceSeat: createWorkspaceSeatFactory({ db: trx }),
getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db: trx }),
createWorkspaceSeat: createWorkspaceSeatFactory({ db: mainDb }),
getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db: mainDb }),
getWorkspaceDefaultSeatType: getWorkspaceDefaultSeatTypeFactory({
getWorkspace: getWorkspaceFactory({ db: trx })
getWorkspace: getWorkspaceFactory({ db: mainDb })
}),
eventEmit: getEventBus().emit
}),
assignWorkspaceSeat: assignWorkspaceSeatFactory({
createWorkspaceSeat: createWorkspaceSeatFactory({ db: trx }),
createWorkspaceSeat: createWorkspaceSeatFactory({ db: mainDb }),
getWorkspaceRoleForUser: getWorkspaceRoleForUserFactory({
db: trx
db: mainDb
}),
eventEmit: getEventBus().emit,
getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db: trx })
getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db: mainDb })
})
}),
findInvite: findInviteFactory({ db: trx }),
deleteInvite: deleteInviteFactory({ db: trx })
findInvite: findInviteFactory({ db: mainDb }),
deleteInvite: deleteInviteFactory({ db: mainDb })
}),
linkUserWithSsoProvider: linkUserWithSsoProviderFactory({
findEmailsByUserId: findEmailsByUserIdFactory({ db: trx }),
createUserEmail: createUserEmailFactory({ db: trx }),
updateUserEmail: updateUserEmailFactory({ db: trx }),
findEmailsByUserId: findEmailsByUserIdFactory({ db: mainDb }),
createUserEmail: createUserEmailFactory({ db: mainDb }),
updateUserEmail: updateUserEmailFactory({ db: mainDb }),
logger: req.log
}),
upsertUserSsoSession: upsertUserSsoSessionFactory({ db: trx })
upsertUserSsoSession: upsertUserSsoSessionFactory({ db: mainDb })
})
await handleOidcCallback(req, res, next)
},
{ db }
{
dbs: await getAllRegisteredDbs(),
logger: req.log,
name: 'oidc callback'
}
)
return next()
@@ -26,6 +26,7 @@ import { beforeEachContext } from '@/test/hooks'
import { Roles } from '@speckle/shared'
import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
import { v4 } from 'uuid'
describe('Workspace workspaceSeat services', () => {
describe('assignWorkspaceSeatFactory', () => {
@@ -34,7 +35,9 @@ describe('Workspace workspaceSeat services', () => {
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.Admin,
verified: true
verified: true,
suuid: v4(),
createdAt: new Date()
}
before(async () => {
@@ -57,7 +60,9 @@ describe('Workspace workspaceSeat services', () => {
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.User,
verified: true
verified: true,
suuid: v4(),
createdAt: new Date()
}
await createTestUser(user)
@@ -85,14 +90,18 @@ describe('Workspace workspaceSeat services', () => {
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.Admin,
verified: true
verified: true,
suuid: v4(),
createdAt: new Date()
}
const testUser: BasicTestUser = {
id: '',
name: createRandomString(),
email: createRandomEmail(),
role: Roles.Server.User,
verified: true
verified: true,
suuid: v4(),
createdAt: new Date()
}
const workspace: BasicTestWorkspace = {
ownerId: '',
+52 -33
View File
@@ -1,6 +1,5 @@
/* eslint-disable no-restricted-imports */
import '../bootstrap.js'
import { db } from '@/db/knex'
import { logger } from '@/observability/logging'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import {
@@ -25,40 +24,12 @@ import {
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import { getEventBus } from '@/modules/shared/services/eventBus'
import axios from 'axios'
const getServerInfo = getServerInfoFactory({ db })
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
import { asMultiregionalOperation } from '@/modules/shared/command.js'
import { getAllRegisteredDbs } from '@/modules/multiregion/utils/dbSelector.js'
const main = async () => {
const userInputs: Array<Parameters<typeof createUser>[0]> = (
const userInputs: Array<Parameters<ReturnType<typeof createUserFactory>>[0]> = (
await axios.get('https://randomuser.me/api/?results=250')
).data.results.map(
(user: {
@@ -74,7 +45,55 @@ const main = async () => {
}
)
await Promise.all(userInputs.map((userInput) => createUser(userInput)))
await Promise.all(
userInputs.map(async (userInput) =>
asMultiregionalOperation(
async ({ mainDb, allDbs, emit }) => {
const createUser = createUserFactory({
getServerInfo: getServerInfoFactory({ db: mainDb }),
findEmail: findEmailFactory({ db: mainDb }),
storeUser: async (...params) => {
const [user] = await Promise.all(
allDbs.map((db) => storeUserFactory({ db })(...params))
)
return user
},
countAdminUsers: countAdminUsersFactory({ db: mainDb }),
storeUserAcl: storeUserAclFactory({ db: mainDb }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db: mainDb }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({
db: mainDb
}),
findEmail: findEmailFactory({ db: mainDb }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db: mainDb }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db: mainDb })
}),
requestNewEmailVerification: requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db: mainDb }),
getUser: getUserFactory({ db: mainDb }),
getServerInfo: getServerInfoFactory({ db: mainDb }),
deleteOldAndInsertNewVerification:
deleteOldAndInsertNewVerificationFactory({ db: mainDb }),
renderEmail,
sendEmail
})
}),
emitEvent: emit
})
return await createUser(userInput)
},
{
logger,
name: 'seedUsers',
dbs: await getAllRegisteredDbs()
}
)
)
)
}
void main()
+69 -34
View File
@@ -29,47 +29,22 @@ import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repos
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { getTestRegionClients } from '@/modules/multiregion/tests/helpers'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { asMultiregionalOperation } from '@/modules/shared/command'
import { logger } from '@/observability/logging'
import { createTestContext, testApolloServer } from '@/test/graphqlHelper'
import { faker } from '@faker-js/faker'
import type { ServerScope } from '@speckle/shared'
import { wait } from '@speckle/shared'
import cryptoRandomString from 'crypto-random-string'
import { assign, isArray, isNumber, omit, times } from 'lodash-es'
import { v4 } from 'uuid'
const getServerInfo = getServerInfoFactory({ db })
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const createPersonalAccessToken = createPersonalAccessTokenFactory({
storeApiToken: storeApiTokenFactory({ db }),
storeTokenScopes: storeTokenScopesFactory({ db }),
@@ -127,10 +102,68 @@ export async function createTestUser(userObj?: Partial<BasicTestUser>) {
setVal('email', createRandomEmail().toLowerCase())
}
const id = await createUser(omit(baseUser, ['id', 'allowPersonalEmail']), {
skipPropertyValidation: true,
allowPersonalEmail: baseUser.allowPersonalEmail
})
if (!baseUser.suuid) {
setVal('suuid', v4())
}
if (typeof baseUser.verified !== 'boolean') {
setVal('verified', false)
}
if (!baseUser.createdAt) {
setVal('createdAt', new Date())
}
const id = await asMultiregionalOperation(
async ({ mainDb, allDbs, emit }) => {
const createUser = createUserFactory({
getServerInfo: getServerInfoFactory({ db: mainDb }),
findEmail: findEmailFactory({ db: mainDb }),
storeUser: async (args) => {
const p = await Promise.all(
allDbs.map(async (db) => storeUserFactory({ db })(args))
)
return p[0]
},
countAdminUsers: countAdminUsersFactory({ db: mainDb }),
storeUserAcl: storeUserAclFactory({ db: mainDb }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db: mainDb }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({
db: mainDb
}),
findEmail: findEmailFactory({ db: mainDb }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db: mainDb }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db: mainDb })
}),
requestNewEmailVerification: requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db: mainDb }),
getUser: getUserFactory({ db: mainDb }),
getServerInfo: getServerInfoFactory({ db: mainDb }),
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory(
{ db: mainDb }
),
renderEmail,
sendEmail
})
}),
emitEvent: emit
})
return await createUser(omit(baseUser, ['id', 'allowPersonalEmail']), {
skipPropertyValidation: true,
allowPersonalEmail: baseUser.allowPersonalEmail
})
},
{
dbs: await getTestRegionClients(),
logger,
name: 'createUser'
}
)
setVal('id', id)
return baseUser
@@ -162,7 +195,9 @@ export const buildBasicTestUser = (overrides?: Partial<BasicTestUser>): BasicTes
id: cryptoRandomString({ length: 10 }),
name: cryptoRandomString({ length: 10 }),
email: createRandomEmail(),
verified: true
verified: true,
createdAt: new Date(),
suuid: v4()
},
overrides
)