diff --git a/packages/server/modules/activitystream/tests/integration/activity.graph.spec.ts b/packages/server/modules/activitystream/tests/integration/activity.graph.spec.ts index c2236702a..8aa735ad7 100644 --- a/packages/server/modules/activitystream/tests/integration/activity.graph.spec.ts +++ b/packages/server/modules/activitystream/tests/integration/activity.graph.spec.ts @@ -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>['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 }) }) diff --git a/packages/server/modules/auth/index.ts b/packages/server/modules/auth/index.ts index 4cc96a2cf..bf36102e2 100644 --- a/packages/server/modules/auth/index.ts +++ b/packages/server/modules/auth/index.ts @@ -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, diff --git a/packages/server/modules/auth/strategies/azureAd.ts b/packages/server/modules/auth/strategies/azureAd.ts index 7bf3121ad..5d51c21a2 100644 --- a/packages/server/modules/auth/strategies/azureAd.ts +++ b/packages/server/modules/auth/strategies/azureAd.ts @@ -40,7 +40,7 @@ const azureAdStrategyBuilderFactory = (deps: { getServerInfo: GetServerInfo getUserByEmail: LegacyGetUserByEmail - findOrCreateUser: FindOrCreateValidatedUser + buildFindOrCreateUser: () => Promise 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 diff --git a/packages/server/modules/auth/strategies/github.ts b/packages/server/modules/auth/strategies/github.ts index f736ac356..afb613b9d 100644 --- a/packages/server/modules/auth/strategies/github.ts +++ b/packages/server/modules/auth/strategies/github.ts @@ -44,7 +44,7 @@ const githubStrategyBuilderFactory = (deps: { getServerInfo: GetServerInfo getUserByEmail: LegacyGetUserByEmail - findOrCreateUser: FindOrCreateValidatedUser + buildFindOrCreateUser: () => Promise 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 diff --git a/packages/server/modules/auth/strategies/google.ts b/packages/server/modules/auth/strategies/google.ts index 747224bce..27cde564d 100644 --- a/packages/server/modules/auth/strategies/google.ts +++ b/packages/server/modules/auth/strategies/google.ts @@ -39,7 +39,7 @@ const googleStrategyBuilderFactory = (deps: { getServerInfo: GetServerInfo getUserByEmail: LegacyGetUserByEmail - findOrCreateUser: FindOrCreateValidatedUser + buildFindOrCreateUser: () => Promise 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 diff --git a/packages/server/modules/auth/strategies/oidc.ts b/packages/server/modules/auth/strategies/oidc.ts index e79410e62..e19de4829 100644 --- a/packages/server/modules/auth/strategies/oidc.ts +++ b/packages/server/modules/auth/strategies/oidc.ts @@ -38,7 +38,7 @@ const oidcStrategyBuilderFactory = (deps: { getServerInfo: GetServerInfo getUserByEmail: LegacyGetUserByEmail - findOrCreateUser: FindOrCreateValidatedUser + buildFindOrCreateUser: () => Promise 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 = 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 diff --git a/packages/server/modules/auth/tests/apps.graphql.spec.ts b/packages/server/modules/auth/tests/apps.graphql.spec.ts index 8fb60c6ad..5e40785ce 100644 --- a/packages/server/modules/auth/tests/apps.graphql.spec.ts +++ b/packages/server/modules/auth/tests/apps.graphql.spec.ts @@ -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>['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, diff --git a/packages/server/modules/auth/tests/apps.spec.ts b/packages/server/modules/auth/tests/apps.spec.ts index c7e18a70d..c53934f12 100644 --- a/packages/server/modules/auth/tests/apps.spec.ts +++ b/packages/server/modules/auth/tests/apps.spec.ts @@ -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, diff --git a/packages/server/modules/auth/tests/auth.spec.ts b/packages/server/modules/auth/tests/auth.spec.ts index 9161bd476..d47f51765 100644 --- a/packages/server/modules/auth/tests/auth.spec.ts +++ b/packages/server/modules/auth/tests/auth.spec.ts @@ -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({ diff --git a/packages/server/modules/blobstorage/tests/e2e/blobstorage.graph.spec.ts b/packages/server/modules/blobstorage/tests/e2e/blobstorage.graph.spec.ts index 49c3cc521..25f12223b 100644 --- a/packages/server/modules/blobstorage/tests/e2e/blobstorage.graph.spec.ts +++ b/packages/server/modules/blobstorage/tests/e2e/blobstorage.graph.spec.ts @@ -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) diff --git a/packages/server/modules/blobstorage/tests/e2e/blobstorage.rest.spec.ts b/packages/server/modules/blobstorage/tests/e2e/blobstorage.rest.spec.ts index 64cdb4171..55564062c 100644 --- a/packages/server/modules/blobstorage/tests/e2e/blobstorage.rest.spec.ts +++ b/packages/server/modules/blobstorage/tests/e2e/blobstorage.rest.spec.ts @@ -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 => { 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 }), diff --git a/packages/server/modules/comments/tests/comments.graph.spec.ts b/packages/server/modules/comments/tests/comments.graph.spec.ts index 255654087..1395e46b8 100644 --- a/packages/server/modules/comments/tests/comments.graph.spec.ts +++ b/packages/server/modules/comments/tests/comments.graph.spec.ts @@ -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 }) diff --git a/packages/server/modules/comments/tests/comments.spec.ts b/packages/server/modules/comments/tests/comments.spec.ts index d41d94432..c7b947420 100644 --- a/packages/server/modules/comments/tests/comments.spec.ts +++ b/packages/server/modules/comments/tests/comments.spec.ts @@ -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 }) diff --git a/packages/server/modules/core/graph/resolvers/users.ts b/packages/server/modules/core/graph/resolvers/users.ts index 73963db92..b302e0794 100644 --- a/packages/server/modules/core/graph/resolvers/users.ts +++ b/packages/server/modules/core/graph/resolvers/users.ts @@ -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: () => ({}) diff --git a/packages/server/modules/core/migrations/20250820101112_drop_user_defaults.ts b/packages/server/modules/core/migrations/20250820101112_drop_user_defaults.ts new file mode 100644 index 000000000..5bc352a7b --- /dev/null +++ b/packages/server/modules/core/migrations/20250820101112_drop_user_defaults.ts @@ -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 { + 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 { + 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() + }) +} diff --git a/packages/server/modules/core/repositories/users.ts b/packages/server/modules/core/repositories/users.ts index ae0533917..5ea5db604 100644 --- a/packages/server/modules/core/repositories/users.ts +++ b/packages/server/modules/core/repositories/users.ts @@ -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 } diff --git a/packages/server/modules/core/services/users/management.ts b/packages/server/modules/core/services/users/management.ts index 40687f4e5..558d52f75 100644 --- a/packages/server/modules/core/services/users/management.ts +++ b/packages/server/modules/core/services/users/management.ts @@ -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, 'suuid' | 'createdAt'> = { + let finalUser: typeof user & NullableKeysToOptional = { ...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() diff --git a/packages/server/modules/core/tests/branches.spec.ts b/packages/server/modules/core/tests/branches.spec.ts index a583e6990..f0c095be3 100644 --- a/packages/server/modules/core/tests/branches.spec.ts +++ b/packages/server/modules/core/tests/branches.spec.ts @@ -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 }) }) diff --git a/packages/server/modules/core/tests/commits.spec.ts b/packages/server/modules/core/tests/commits.spec.ts index 05b5adb6a..01a68bc04 100644 --- a/packages/server/modules/core/tests/commits.spec.ts +++ b/packages/server/modules/core/tests/commits.spec.ts @@ -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 }) diff --git a/packages/server/modules/core/tests/favoriteStreams.spec.ts b/packages/server/modules/core/tests/favoriteStreams.spec.ts index 9a9a3cc38..a2089ca40 100644 --- a/packages/server/modules/core/tests/favoriteStreams.spec.ts +++ b/packages/server/modules/core/tests/favoriteStreams.spec.ts @@ -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( diff --git a/packages/server/modules/core/tests/generic.spec.ts b/packages/server/modules/core/tests/generic.spec.ts index 9fd764615..06d33d03c 100644 --- a/packages/server/modules/core/tests/generic.spec.ts +++ b/packages/server/modules/core/tests/generic.spec.ts @@ -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( diff --git a/packages/server/modules/core/tests/graph.spec.ts b/packages/server/modules/core/tests/graph.spec.ts index 4b0d9d816..49152255f 100644 --- a/packages/server/modules/core/tests/graph.spec.ts +++ b/packages/server/modules/core/tests/graph.spec.ts @@ -24,31 +24,10 @@ import { import { getUserFactory, legacyGetPaginatedUsersFactory, - storeUserFactory, - countAdminUsersFactory, - storeUserAclFactory, isLastAdminUserFactory, updateUserServerRoleFactory } 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, - changeUserRoleFactory -} 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 { changeUserRoleFactory } from '@/modules/core/services/users/management' import { createPersonalAccessTokenFactory } from '@/modules/core/services/tokens' import { storeApiTokenFactory, @@ -72,6 +51,8 @@ import { type TestApolloServer } from '@/test/graphqlHelper' import { AllScopes } from '@/modules/core/helpers/mainConstants' +import type { BasicTestUser } from '@/test/authHelper' +import { createTestUser } from '@/test/authHelper' const { FF_PERSONAL_PROJECTS_LIMITS_ENABLED, FF_USERS_INVITE_SCOPE_IS_PUBLIC } = getFeatureFlags() @@ -100,33 +81,6 @@ const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({ const getUsers = legacyGetPaginatedUsersFactory({ db }) 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 }), @@ -151,27 +105,13 @@ const changeUserRole = changeUserRoleFactory({ ;(FF_PERSONAL_PROJECTS_LIMITS_ENABLED ? describe.skip : describe)( 'GraphQL API Core @core-api (Legacy)', () => { - const userA = { - id: '', - token: '', - name: 'd1', - email: 'd.1@speckle.systems', - password: 'wowwowwowwowwow' - } - const userB = { - id: '', - token: '', - name: 'd2', - email: 'd.2@speckle.systems', - password: 'wowwowwowwowwow' - } - const userC = { - id: '', - token: '', - name: 'd3', - email: 'd.3@speckle.systems', - password: 'wowwowwowwowwow' - } + let userA: BasicTestUser + let userB: BasicTestUser + let userC: BasicTestUser + + let tokenUserA: string + let tokenUserB: string + let tokenUserC: string // set up app & two basic users to ping pong permissions around before(async () => { @@ -180,8 +120,14 @@ const changeUserRole = changeUserRoleFactory({ app = ctx.app ;({ sendRequest } = await initializeTestServer(ctx)) - userA.id = await createUser(userA) - userA.token = `Bearer ${await createPersonalAccessToken( + userA = await createTestUser({ + id: '', + name: 'd1', + email: 'd.1@speckle.systems', + password: 'wowwowwowwowwow' + }) + + tokenUserA = `Bearer ${await createPersonalAccessToken( userA.id, 'test token user A', [ @@ -196,8 +142,15 @@ const changeUserRole = changeUserRoleFactory({ Scopes.Profile.Email ] )}` - userB.id = await createUser(userB) - userB.token = `Bearer ${await createPersonalAccessToken( + + userB = await createTestUser({ + id: '', + name: 'd2', + email: 'd.2@speckle.systems', + password: 'wowwowwowwowwow' + }) + + tokenUserB = `Bearer ${await createPersonalAccessToken( userB.id, 'test token user B', [ @@ -211,8 +164,13 @@ const changeUserRole = changeUserRoleFactory({ Scopes.Profile.Email ] )}` - userC.id = await createUser(userC) - userC.token = `Bearer ${await createPersonalAccessToken( + userC = await createTestUser({ + id: '', + name: 'd3', + email: 'd.3@speckle.systems', + password: 'wowwowwowwowwow' + }) + tokenUserC = `Bearer ${await createPersonalAccessToken( userC.id, 'test token user B', [ @@ -238,19 +196,19 @@ const changeUserRole = changeUserRoleFactory({ }) // Prepare API tokens for use in tests - const res1 = await sendRequest(userA.token, { + const res1 = await sendRequest(tokenUserA, { query: 'mutation { apiTokenCreate(token: {name:"Token 1", scopes: ["streams:read", "users:read", "tokens:read"]}) }' }) token1 = `Bearer ${res1.body.data.apiTokenCreate}` - const res2 = await sendRequest(userA.token, { + const res2 = await sendRequest(tokenUserA, { query: 'mutation { apiTokenCreate(token: {name:"Token 1", scopes: ["streams:write", "streams:read", "users:email"]}) }' }) token2 = `Bearer ${res2.body.data.apiTokenCreate}` - const res3 = await sendRequest(userB.token, { + const res3 = await sendRequest(tokenUserB, { query: 'mutation { apiTokenCreate(token: {name:"Token 1", scopes: ["streams:write", "streams:read", "users:email"]}) }' }) @@ -376,7 +334,7 @@ const changeUserRole = changeUserRoleFactory({ describe('Mutations', () => { describe('Users & Api tokens', () => { it('Should create some api tokens', async () => { - const res1 = await sendRequest(userA.token, { + const res1 = await sendRequest(tokenUserA, { query: 'mutation { apiTokenCreate(token: {name:"Token 1", scopes: ["streams:read", "users:read", "tokens:read"]}) }' }) @@ -385,13 +343,13 @@ const changeUserRole = changeUserRoleFactory({ expect(res1.body.data.apiTokenCreate).to.be.a('string') token1 = `Bearer ${res1.body.data.apiTokenCreate}` - const res2 = await sendRequest(userA.token, { + const res2 = await sendRequest(tokenUserA, { query: 'mutation { apiTokenCreate(token: {name:"Token 1", scopes: ["streams:write", "streams:read", "users:email"]}) }' }) token2 = `Bearer ${res2.body.data.apiTokenCreate}` - const res3 = await sendRequest(userB.token, { + const res3 = await sendRequest(tokenUserB, { query: 'mutation { apiTokenCreate(token: {name:"Token 1", scopes: ["streams:write", "streams:read", "users:email"]}) }' }) @@ -399,7 +357,7 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should revoke an api token that the user owns', async () => { - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: `mutation{ apiTokenRevoke(token:"${token2}")}` }) expect(res).to.be.json @@ -408,7 +366,7 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should fail to revoke an api token that I do not own', async () => { - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: `mutation{ apiTokenRevoke(token:"${token3}")}` }) expect(res).to.be.json @@ -427,7 +385,7 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should edit my profile', async () => { - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: 'mutation($user:UserUpdateInput!) { userUpdate( user: $user) } ', variables: { user: { @@ -442,16 +400,14 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should delete my account', async () => { - const userDelete = { + const userDelete = await createTestUser({ id: '', - token: '', name: 'delete', email: `${cryptoRandomString({ length: 10 })}@example.org`, password: 'wowwowwowwowwow' - } - userDelete.id = await createUser(userDelete) + }) - userDelete.token = `Bearer ${await createPersonalAccessToken( + let userDeleteToken = `Bearer ${await createPersonalAccessToken( userDelete.id, 'fail token user del', [ @@ -466,7 +422,7 @@ const changeUserRole = changeUserRoleFactory({ ] )}` - const badTokenScopesBadEmail = await sendRequest(userDelete.token, { + const badTokenScopesBadEmail = await sendRequest(userDeleteToken, { query: 'mutation($user:UserDeleteInput!) { userDelete( userConfirmation: $user) } ', variables: { user: { email: 'wrongEmail@email.com' } } @@ -475,7 +431,7 @@ const changeUserRole = changeUserRoleFactory({ expect(badTokenScopesBadEmail.body.errors[0].extensions?.code).to.equal( 'FORBIDDEN' ) - const badTokenScopesGoodEmail = await sendRequest(userDelete.token, { + const badTokenScopesGoodEmail = await sendRequest(userDeleteToken, { query: 'mutation($user:UserDeleteInput!) { userDelete( userConfirmation: $user) } ', variables: { user: { email: userDelete.email } } @@ -485,7 +441,7 @@ const changeUserRole = changeUserRoleFactory({ 'FORBIDDEN' ) - userDelete.token = `Bearer ${await createPersonalAccessToken( + userDeleteToken = `Bearer ${await createPersonalAccessToken( userDelete.id, 'test token user del', [ @@ -501,7 +457,7 @@ const changeUserRole = changeUserRoleFactory({ ] )}` - const goodTokenScopesBadEmail = await sendRequest(userDelete.token, { + const goodTokenScopesBadEmail = await sendRequest(userDeleteToken, { query: 'mutation($user:UserDeleteInput!) { userDelete( userConfirmation: $user) } ', variables: { user: { email: 'wrongEmail@email.com' } } @@ -510,7 +466,7 @@ const changeUserRole = changeUserRoleFactory({ expect(goodTokenScopesBadEmail.body.errors[0].extensions?.code).to.equal( 'BAD_REQUEST_ERROR' ) - const goodTokenScopesGoodEmail = await sendRequest(userDelete.token, { + const goodTokenScopesGoodEmail = await sendRequest(userDeleteToken, { query: 'mutation($user:UserDeleteInput!) { userDelete( userConfirmation: $user) } ', variables: { user: { email: userDelete.email } } @@ -521,20 +477,20 @@ const changeUserRole = changeUserRoleFactory({ describe('User role change', () => { it('User role is changed', async () => { - let queriedUserB = await sendRequest(userA.token, { + let queriedUserB = await sendRequest(tokenUserA, { query: ` { otherUser(id:"${userB.id}") { id name role } }` }) expect(queriedUserB.body.data.otherUser.role).to.equal(Roles.Server.User) let query = `mutation { userRoleChange(userRoleInput: {id: "${userB.id}", role: "${Roles.Server.Admin}"})}` - await sendRequest(userA.token, { query }) - queriedUserB = await sendRequest(userA.token, { + await sendRequest(tokenUserA, { query }) + queriedUserB = await sendRequest(tokenUserA, { query: ` { otherUser(id:"${userB.id}") { id name role } }` }) expect(queriedUserB.body.data.otherUser.role).to.equal(Roles.Server.Admin) expect(queriedUserB.body.data) query = `mutation { userRoleChange(userRoleInput: {id: "${userB.id}", role: "${Roles.Server.User}"})}` - await sendRequest(userA.token, { query }) - queriedUserB = await sendRequest(userA.token, { + await sendRequest(tokenUserA, { query }) + queriedUserB = await sendRequest(tokenUserA, { query: ` { otherUser(id:"${userB.id}") { id name role } }` }) expect(queriedUserB.body.data.otherUser.role).to.equal(Roles.Server.User) @@ -542,8 +498,8 @@ const changeUserRole = changeUserRoleFactory({ it('Only admins can change user role', async () => { const query = `mutation { userRoleChange(userRoleInput: {id: "${userB.id}", role: "${Roles.Server.Admin}"})}` - const res = await sendRequest(userB.token, { query }) - const queriedUserB = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserB, { query }) + const queriedUserB = await sendRequest(tokenUserA, { query: ` { otherUser(id:"${userB.id}") { id name role } }` }) expect(res.body.errors).to.exist @@ -554,35 +510,33 @@ const changeUserRole = changeUserRoleFactory({ describe('User deletion', () => { it('Only admins can delete user', async () => { - const userDelete = { + const userDelete = await createTestUser({ id: '', name: 'delete', email: `${cryptoRandomString({ length: 10 })}@example.org`, password: 'wowwowwowwowwow' - } - userDelete.id = await createUser(userDelete) + }) const users = await getUsers() expect(users.map((u) => u.id)).to.contain(userDelete.id) const query = `mutation { adminDeleteUser( userConfirmation: { email: "${userDelete.email}" } ) } ` - const res = await sendRequest(userB.token, { query }) + const res = await sendRequest(tokenUserB, { query }) expect(res.body.errors).to.exist expect(res.body.errors[0].extensions.code).to.equal('FORBIDDEN') }) it('Admin can delete user', async () => { - const userDelete = { + const userDelete = await createTestUser({ id: '', name: 'delete', email: 'd3l3t3@speckle.systems', password: 'wowwowwowwowwow' - } - userDelete.id = await createUser(userDelete) + }) let users = await getUsers() expect(users.map((u) => u.id)).to.contain(userDelete.id) const query = `mutation { adminDeleteUser( userConfirmation: { email: "${userDelete.email}" } ) } ` - const deleteResult = await sendRequest(userA.token, { query }) + const deleteResult = await sendRequest(tokenUserA, { query }) expect(deleteResult.body.data.adminDeleteUser).to.equal(true) users = await getUsers() expect(users.map((u) => u.id)).to.not.contain(userDelete.id) @@ -590,7 +544,7 @@ const changeUserRole = changeUserRoleFactory({ it('Cannot delete the last admin', async () => { const query = `mutation { adminDeleteUser( userConfirmation: { email: "${userA.email}" } ) } ` - const res = await sendRequest(userA.token, { query }) + const res = await sendRequest(tokenUserA, { query }) expect(res.body.errors).to.exist expect(res.body.errors[0].extensions?.code).to.equal('USER_INPUT_ERROR') expect(res.body.errors[0].message).to.equal( @@ -665,7 +619,7 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should create some streams', async () => { - const resS1 = await sendRequest(userA.token, { + const resS1 = await sendRequest(tokenUserA, { query: 'mutation { streamCreate(stream: { name: "TS1 (u A) Private", description: "Hello World", isPublic:false } ) }' }) @@ -676,7 +630,7 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should update a stream', async () => { - const resS1 = await sendRequest(userA.token, { + const resS1 = await sendRequest(tokenUserA, { query: `mutation { streamUpdate(stream: {id:"${ts1}" name: "TS1 (u A) Private UPDATED", description: "Hello World, Again!", isPublic:false } ) }` }) @@ -693,7 +647,7 @@ const changeUserRole = changeUserRoleFactory({ publicPrivateDataset.forEach(({ display, isPublic }) => { it(`Should not allow updating permissions if target user isnt a collaborator on a ${display} stream`, async () => { const streamId = isPublic ? ts2 : ts1 - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: `mutation{ streamUpdatePermission( permissionParams: {streamId: "${streamId}", userId: "${userB.id}" role: "stream:owner"}) }` }) @@ -713,7 +667,7 @@ const changeUserRole = changeUserRoleFactory({ Roles.Stream.Reviewer, userA.id ) - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: `mutation{ streamUpdatePermission( permissionParams: {streamId: "${ts1}", userId: "${userB.id}" role: "stream:owner"}) }` }) @@ -727,7 +681,7 @@ const changeUserRole = changeUserRoleFactory({ Roles.Stream.Reviewer, userB.id ) - const res2 = await sendRequest(userB.token, { + const res2 = await sendRequest(tokenUserB, { query: `mutation{ streamUpdatePermission( permissionParams: {streamId: "${ts5}", userId: "${userA.id}" role: "stream:owner"}) }` }) expect(res2).to.be.json @@ -739,7 +693,7 @@ const changeUserRole = changeUserRoleFactory({ Roles.Stream.Reviewer, userB.id ) - const res3 = await sendRequest(userB.token, { + const res3 = await sendRequest(tokenUserB, { query: `mutation{ streamUpdatePermission( permissionParams: {streamId: "${ts3}", userId: "${userC.id}" role: "stream:owner"}) }` }) expect(res3).to.be.json @@ -747,7 +701,7 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should fail to grant permissions if not owner', async () => { - const res = await sendRequest(userB.token, { + const res = await sendRequest(tokenUserB, { query: `mutation{ streamUpdatePermission( permissionParams: {streamId: "${ts1}", userId: "${userB.id}" role: "stream:owner"}) }` }) expect(res).to.be.json @@ -758,7 +712,7 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should fail to grant myself permissions', async () => { - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: `mutation{ streamUpdatePermission( permissionParams: {streamId: "${ts1}", userId: "${userA.id}" role: "stream:owner"}) }` }) expect(res).to.be.json @@ -777,21 +731,21 @@ const changeUserRole = changeUserRoleFactory({ ) // first test if we can get it - const res = await sendRequest(userC.token, { + const res = await sendRequest(tokenUserC, { query: `query { stream(id:"${ts3}") { id name } }` }) expect(res).to.be.json expect(res.body.errors).to.not.exist expect(res.body.data.stream.name).to.equal('TS3 (u B) Private') - const revokeRes = await sendRequest(userB.token, { + const revokeRes = await sendRequest(tokenUserB, { query: `mutation { streamRevokePermission( permissionParams: {streamId: "${ts3}", userId:"${userC.id}"} ) }` }) expect(revokeRes).to.be.json expect(revokeRes.body.errors).to.not.exist expect(revokeRes.body.data.streamRevokePermission).to.equal(true) - const resNotAuth = await sendRequest(userC.token, { + const resNotAuth = await sendRequest(tokenUserC, { query: `query { stream(id:"${ts3}") { id name role } }` }) expect(resNotAuth).to.be.json @@ -801,7 +755,7 @@ const changeUserRole = changeUserRoleFactory({ it('Should fail to edit/write on a public stream if no access is provided', async () => { // ts4 is a public stream from uesrB - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: `mutation { streamUpdate(stream: {id:"${ts4}" name: "HACK", description: "Hello World, Again!", isPublic:false } ) }` }) expect(res.body.errors).to.exist @@ -809,7 +763,7 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should fail editing a private stream if no access has been granted', async () => { - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: `mutation { streamUpdate(stream: {id:"${ts3}" name: "HACK", description: "Hello World, Again!", isPublic:false } ) }` }) @@ -821,7 +775,7 @@ const changeUserRole = changeUserRoleFactory({ // Make sure user is no longer a stream collaborator await removeStreamCollaborator(ts1, userB.id, userB.id) - const res = await sendRequest(userB.token, { + const res = await sendRequest(tokenUserB, { query: `mutation { streamDelete( id:"${ts1}")}` }) expect(res).to.be.json @@ -831,7 +785,7 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should fail to delete streams if not admin', async () => { - const res = await sendRequest(userB.token, { + const res = await sendRequest(tokenUserB, { query: `mutation { streamsDelete( ids:"[${ts4}]")}` }) expect(res).to.be.json @@ -841,7 +795,7 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should delete a stream', async () => { - const res = await sendRequest(userB.token, { + const res = await sendRequest(tokenUserB, { query: `mutation { streamDelete( id:"${ts4}")}` }) @@ -852,7 +806,7 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should be forbidden to query admin streams if not admin', async () => { - const res = await sendRequest(userC.token, { + const res = await sendRequest(tokenUserC, { query: '{ adminStreams { totalCount items { id name } } }' }) expect(res).to.be.json @@ -862,13 +816,13 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should query admin streams', async () => { - let streamResults = await sendRequest(userA.token, { + let streamResults = await sendRequest(tokenUserA, { query: '{ adminStreams { totalCount items { id name } } }' }) expect(streamResults.body.data.adminStreams.totalCount).to.equal(10) - streamResults = await sendRequest(userA.token, { + streamResults = await sendRequest(tokenUserA, { query: '{ adminStreams(limit: 200) { totalCount items { id name } } }' }) expect(streamResults.body.errors).to.exist @@ -876,18 +830,18 @@ const changeUserRole = changeUserRoleFactory({ 'BAD_REQUEST_ERROR' ) - streamResults = await sendRequest(userA.token, { + streamResults = await sendRequest(tokenUserA, { query: '{ adminStreams(limit: 2) { totalCount items { id name } } }' }) expect(streamResults.body.data.adminStreams.totalCount).to.equal(10) expect(streamResults.body.data.adminStreams.items.length).to.equal(2) - streamResults = await sendRequest(userA.token, { + streamResults = await sendRequest(tokenUserA, { query: '{ adminStreams( query: "Admin" ) { totalCount items { id name } } }' }) expect(streamResults.body.data.adminStreams.totalCount).to.equal(5) - streamResults = await sendRequest(userA.token, { + streamResults = await sendRequest(tokenUserA, { query: '{ adminStreams( visibility: "private" ) { totalCount items { id name isPublic } } }' }) @@ -896,7 +850,7 @@ const changeUserRole = changeUserRoleFactory({ streams.every((stream) => !stream.isPublic) ) - streamResults = await sendRequest(userA.token, { + streamResults = await sendRequest(tokenUserA, { query: '{ adminStreams( visibility: "public" ) { totalCount items { id name isPublic } } }' }) @@ -907,14 +861,14 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should delete streams', async () => { - const streamResults = await sendRequest(userA.token, { + const streamResults = await sendRequest(tokenUserA, { query: '{ adminStreams( query: "Admin" ) { totalCount items { id name } } }' }) expect(streamResults.body.data.adminStreams.totalCount).to.equal(5) const streamIds = streamResults.body.data.adminStreams.items.map( (stream: { id: string }) => stream.id ) - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: 'mutation ( $ids: [String!] ){ streamsDelete( ids: $ids )}', variables: { ids: streamIds } }) @@ -963,7 +917,7 @@ const changeUserRole = changeUserRoleFactory({ }) } - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: `mutation( $objs: [JSONObject]! ) { objectCreate( objectInput: {streamId:"${ts1}", objects: $objs} ) }`, variables: { objs } }) @@ -981,7 +935,7 @@ const changeUserRole = changeUserRoleFactory({ c1.objectId = objIds[0] c1.branchName = 'main' - let res = await sendRequest(userA.token, { + let res = await sendRequest(tokenUserA, { query: 'mutation( $myCommit: CommitCreateInput! ) { commitCreate( commit: $myCommit ) }', variables: { myCommit: omit(c1, 'id') } @@ -999,7 +953,7 @@ const changeUserRole = changeUserRoleFactory({ c2.branchName = 'main' c2.previousCommitIds = [c1.id] - res = await sendRequest(userA.token, { + res = await sendRequest(tokenUserA, { query: 'mutation( $myCommit: CommitCreateInput! ) { commitCreate( commit: $myCommit ) }', variables: { myCommit: omit(c2, 'id') } @@ -1018,7 +972,7 @@ const changeUserRole = changeUserRoleFactory({ id: c1.id, message: 'first commit' } - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: 'mutation( $myCommit: CommitUpdateInput! ) { commitUpdate( commit: $myCommit ) }', variables: { myCommit: updatePayload } @@ -1027,7 +981,7 @@ const changeUserRole = changeUserRoleFactory({ expect(res.body.errors).to.not.exist expect(res.body.data).to.have.property('commitUpdate') - const res2 = await sendRequest(userB.token, { + const res2 = await sendRequest(tokenUserB, { query: 'mutation( $myCommit: CommitUpdateInput! ) { commitUpdate( commit: $myCommit ) }', variables: { myCommit: updatePayload } @@ -1038,7 +992,7 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should create a read receipt', async () => { - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: 'mutation($input: CommitReceivedInput!) { commitReceive(input: $input) }', variables: { @@ -1076,7 +1030,7 @@ const changeUserRole = changeUserRoleFactory({ it('Should delete a commit', async () => { const payload = { streamId: ts1, id: c2.id } - const res = await sendRequest(userB.token, { + const res = await sendRequest(tokenUserB, { query: 'mutation( $myCommit: CommitDeleteInput! ) { commitDelete( commit: $myCommit ) }', variables: { myCommit: payload } @@ -1085,7 +1039,7 @@ const changeUserRole = changeUserRoleFactory({ expect(res.body.errors).to.exist expect(res.body.errors[0].extensions?.code).to.equal('FORBIDDEN') - const res2 = await sendRequest(userA.token, { + const res2 = await sendRequest(tokenUserA, { query: 'mutation( $myCommit: CommitDeleteInput! ) { commitDelete( commit: $myCommit ) }', variables: { myCommit: payload } @@ -1103,7 +1057,7 @@ const changeUserRole = changeUserRoleFactory({ description: 'dimitries development branch' } - const res1 = await sendRequest(userA.token, { + const res1 = await sendRequest(tokenUserA, { query: 'mutation( $branch:BranchCreateInput! ) { branchCreate( branch:$branch ) }', variables: { branch: omit(b1, 'id') } @@ -1127,7 +1081,7 @@ const changeUserRole = changeUserRoleFactory({ Roles.Stream.Contributor, userA.id ) - const res2 = await sendRequest(userB.token, { + const res2 = await sendRequest(tokenUserB, { query: 'mutation( $branch:BranchCreateInput! ) { branchCreate( branch:$branch ) }', variables: { branch: omit(b2, 'id') } @@ -1141,7 +1095,7 @@ const changeUserRole = changeUserRoleFactory({ name: 'userB/dev/api', description: 'more branches branch' } - const res3 = await sendRequest(userB.token, { + const res3 = await sendRequest(tokenUserB, { query: 'mutation( $branch:BranchCreateInput! ) { branchCreate( branch:$branch ) }', variables: { branch: omit(b3, 'id') } @@ -1157,7 +1111,7 @@ const changeUserRole = changeUserRoleFactory({ name: 'randomupdateablebranch', description: 'dimitries development branch' } - const b1Res = await sendRequest(userA.token, { + const b1Res = await sendRequest(tokenUserA, { query: 'mutation( $branch:BranchCreateInput! ) { branchCreate( branch:$branch ) }', variables: { branch: omit(b1, 'id') } @@ -1172,7 +1126,7 @@ const changeUserRole = changeUserRoleFactory({ name: 'userb/whatever/whatever' } - const res1 = await sendRequest(userA.token, { + const res1 = await sendRequest(tokenUserA, { query: 'mutation( $branch:BranchUpdateInput! ) { branchUpdate( branch:$branch ) }', variables: { branch: payload } @@ -1190,7 +1144,7 @@ const changeUserRole = changeUserRoleFactory({ name: 'randomudeletablebranch', description: 'dimitries development branch' } - const b1Res = await sendRequest(userA.token, { + const b1Res = await sendRequest(tokenUserA, { query: 'mutation( $branch:BranchCreateInput! ) { branchCreate( branch:$branch ) }', variables: { branch: omit(b1, 'id') } @@ -1217,7 +1171,7 @@ const changeUserRole = changeUserRoleFactory({ id: 'APRIL FOOOLS!' } - const res = await sendRequest(userC.token, { + const res = await sendRequest(tokenUserC, { query: 'mutation( $branch:BranchDeleteInput! ) { branchDelete( branch: $branch ) }', variables: { branch: badPayload } @@ -1226,7 +1180,7 @@ const changeUserRole = changeUserRoleFactory({ expect(res.body.errors).to.exist expect(res.body.errors[0].extensions.code).to.equal('NOT_FOUND_ERROR') - const res1 = await sendRequest(userC.token, { + const res1 = await sendRequest(tokenUserC, { query: 'mutation( $branch:BranchDeleteInput! ) { branchDelete( branch: $branch ) }', variables: { branch: payload } @@ -1235,7 +1189,7 @@ const changeUserRole = changeUserRoleFactory({ expect(res1.body.errors).to.exist expect(res1.body.errors[0].extensions.code).to.equal('FORBIDDEN') - const res2 = await sendRequest(userA.token, { + const res2 = await sendRequest(tokenUserA, { query: 'mutation( $branch:BranchDeleteInput! ) { branchDelete( branch: $branch ) }', variables: { branch: payload } @@ -1244,7 +1198,7 @@ const changeUserRole = changeUserRoleFactory({ expect(res2.body.errors).to.not.exist // revoke perms for c back (dont' wanna mess up our integration-unit tests below) - await sendRequest(userA.token, { + await sendRequest(tokenUserA, { query: `mutation{ streamRevokePermission( permissionParams: {streamId: "${ts1}", userId: "${userC.id}"} ) }` }) }) @@ -1257,7 +1211,7 @@ const changeUserRole = changeUserRoleFactory({ branchName: 'userB/dev/api' } - const res = await sendRequest(userB.token, { + const res = await sendRequest(tokenUserB, { query: 'mutation( $myCommit: CommitCreateInput! ) { commitCreate( commit: $myCommit ) }', variables: { myCommit: cc } @@ -1270,7 +1224,7 @@ const changeUserRole = changeUserRoleFactory({ it('Should *not* update a branch if given the wrong stream id', async () => { // create stream for user C - const res = await sendRequest(userC.token, { + const res = await sendRequest(tokenUserC, { query: 'mutation { streamCreate(stream: { name: "TS (u C) private", description: "sup my dudes", isPublic:false } ) }' }) @@ -1285,7 +1239,7 @@ const changeUserRole = changeUserRoleFactory({ name: 'izz/secret', description: 'a private branch on a private stream' } - const res1 = await sendRequest(userB.token, { + const res1 = await sendRequest(tokenUserB, { query: 'mutation( $branch:BranchCreateInput! ) { branchCreate( branch:$branch ) }', variables: { branch: omit(b4, 'id') } @@ -1302,7 +1256,7 @@ const changeUserRole = changeUserRoleFactory({ name: 'izz/not-so-secret' } - const res2 = await sendRequest(userC.token, { + const res2 = await sendRequest(tokenUserC, { query: 'mutation( $branch:BranchUpdateInput! ) { branchUpdate( branch:$branch ) }', variables: { branch: badPayload } @@ -1321,7 +1275,7 @@ const changeUserRole = changeUserRoleFactory({ id: b4.id // branch user C doesn't have access to } - const res = await sendRequest(userC.token, { + const res = await sendRequest(tokenUserC, { query: 'mutation( $branch:BranchDeleteInput! ) { branchDelete( branch: $branch ) }', variables: { branch: badPayload } @@ -1339,7 +1293,7 @@ const changeUserRole = changeUserRoleFactory({ describe('Queries', () => { describe('My Profile', () => { it('Should retrieve my profile', async () => { - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: '{ user { id name email role apiTokens { id name } } }' }) expect(res).to.be.json @@ -1352,25 +1306,25 @@ const changeUserRole = changeUserRoleFactory({ it('Should retrieve my streams', async () => { // add more streams - await sendRequest(userA.token, { + await sendRequest(tokenUserA, { query: 'mutation( $myStream: StreamCreateInput! ) { streamCreate( stream: $myStream ) }', variables: { myStream: { name: 'o hai' } } }) - await sendRequest(userA.token, { + await sendRequest(tokenUserA, { query: 'mutation( $myStream: StreamCreateInput! ) { streamCreate( stream: $myStream ) }', variables: { myStream: { name: 'bai now' } } }) - await sendRequest(userA.token, { + await sendRequest(tokenUserA, { query: 'mutation( $myStream: StreamCreateInput! ) { streamCreate( stream: $myStream ) }', variables: { myStream: { name: 'one more for the road' } } }) - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: '{ user { streams( limit: 3 ) { totalCount cursor items { id name } } } }' }) @@ -1378,7 +1332,7 @@ const changeUserRole = changeUserRoleFactory({ expect(res.body.errors).to.not.exist expect(res.body.data.user.streams.items.length).to.equal(3) - const res2 = await sendRequest(userA.token, { + const res2 = await sendRequest(tokenUserA, { query: `{ user { streams( limit: 3, cursor: "${res.body.data.user.streams.cursor}" ) { totalCount cursor items { id name } } } }` }) expect(res2).to.be.json @@ -1400,14 +1354,14 @@ const changeUserRole = changeUserRoleFactory({ objectId: objIds[i], branchName: 'main' } - await sendRequest(userA.token, { + await sendRequest(tokenUserA, { query: 'mutation( $myCommit: CommitCreateInput! ) { commitCreate( commit: $myCommit ) }', variables: { myCommit: c1 } }) } - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: '{ user { commits( limit: 3 ) { totalCount cursor items { id message referencedObject } } } }' }) @@ -1418,7 +1372,7 @@ const changeUserRole = changeUserRoleFactory({ expect(res.body.data.user.commits.cursor).to.exist expect(res.body.data.user.commits.items.length).to.equal(3) - const res2 = await sendRequest(userA.token, { + const res2 = await sendRequest(tokenUserA, { query: `{ user { commits( limit: 3, cursor: "${res.body.data.user.commits.cursor}") { totalCount cursor items { id message referencedObject } } } }` }) expect(res2).to.be.json @@ -1434,7 +1388,7 @@ const changeUserRole = changeUserRoleFactory({ */ it('Should retrieve a different profile profile', async () => { - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: ` { user(id:"${userB.id}") { id name email } }` }) @@ -1476,7 +1430,7 @@ const changeUserRole = changeUserRoleFactory({ it('Should search for some users', async () => { for (let i = 0; i < 10; i++) { // create 10 users: 3 bakers and 7 millers - await createUser({ + await createTestUser({ name: `Master ${i <= 2 ? 'Baker' : 'Miller'} Matteo The ${i}${ i === 1 ? 'st' : i === 2 ? 'nd' : i === 3 ? 'rd' : 'th' } of His Name`, @@ -1499,7 +1453,7 @@ const changeUserRole = changeUserRoleFactory({ } ` - let res = await sendRequest(userB.token, { query }) + let res = await sendRequest(tokenUserB, { query }) expect(res).to.be.json expect(res.body.errors).to.not.exist expect(res.body.data.userSearch.items.length).to.equal(7) @@ -1516,7 +1470,7 @@ const changeUserRole = changeUserRoleFactory({ } ` - res = await sendRequest(userB.token, { query }) + res = await sendRequest(tokenUserB, { query }) expect(res).to.be.json expect(res.body.errors).to.not.exist expect(res.body.data.userSearch.items.length).to.equal(3) @@ -1524,7 +1478,7 @@ const changeUserRole = changeUserRoleFactory({ // by email query = 'query { userSearch( query: "matteo_2@tomato.com" ) { cursor items { id name } } } ' - res = await sendRequest(userB.token, { query }) + res = await sendRequest(tokenUserB, { query }) expect(res).to.be.json expect(res.body.errors).to.not.exist expect(res.body.data.userSearch.items.length).to.equal(1) @@ -1533,14 +1487,14 @@ const changeUserRole = changeUserRoleFactory({ it('Should not search for some users if bad request', async () => { const queryLim = 'query { userSearch( query: "mi" ) { cursor items { id name } } } ' - let res = await sendRequest(userB.token, { query: queryLim }) + let res = await sendRequest(tokenUserB, { query: queryLim }) expect(res).to.be.json expect(res.body.errors).to.exist expect(res.body.errors[0].extensions.code).to.equal('BAD_REQUEST_ERROR') const queryPagination = 'query { userSearch( query: "matteo", limit: 200 ) { cursor items { id name } } } ' - res = await sendRequest(userB.token, { query: queryPagination }) + res = await sendRequest(tokenUserB, { query: queryPagination }) expect(res).to.be.json expect(res.body.errors).to.exist expect(res.body.errors[0].extensions.code).to.equal('BAD_REQUEST_ERROR') @@ -1549,7 +1503,7 @@ const changeUserRole = changeUserRoleFactory({ describe('Streams', () => { it('Should retrieve a stream', async () => { - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: ` query { stream(id:"${ts1}") { @@ -1613,7 +1567,7 @@ const changeUserRole = changeUserRoleFactory({ } ` - const res = await sendRequest(userA.token, { query }) + const res = await sendRequest(tokenUserA, { query }) expect(res).to.be.json expect(res.body.errors).to.not.exist expect(res.body.data.stream.branches.items).to.be.ok @@ -1621,7 +1575,7 @@ const changeUserRole = changeUserRoleFactory({ expect(res.body.data.stream.branches.cursor).to.exist const firstBranchName = res.body.data.stream.branches.items[0].name - const res1 = await sendRequest(userA.token, { + const res1 = await sendRequest(tokenUserA, { query: `query { stream(id:"${ts1}") { branch( name: "${firstBranchName}" ) { name description } } } ` }) @@ -1631,7 +1585,7 @@ const changeUserRole = changeUserRoleFactory({ }) it("it should retrieve a stream's default 'main' branch if no branch name is specified", async () => { - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: `query { stream(id:"${ts1}") { branch { name description } } } ` }) expect(res).to.be.json @@ -1661,7 +1615,7 @@ const changeUserRole = changeUserRoleFactory({ } } ` - const res = await sendRequest(userA.token, { query }) + const res = await sendRequest(tokenUserA, { query }) expect(res.body.data.stream.branch.commits.items.length).to.equal(5) expect(res.body.data.stream.branch.commits.items[0]).to.have.property('id') expect(res.body.data.stream.branch.commits.items[0]).to.have.property( @@ -1693,7 +1647,7 @@ const changeUserRole = changeUserRoleFactory({ } }` - const res2 = await sendRequest(userA.token, { query: query2 }) + const res2 = await sendRequest(tokenUserA, { query: query2 }) // console.log( res2.body.errors ) // console.log( res2.body.data.stream.branch.commits ) @@ -1726,7 +1680,7 @@ const changeUserRole = changeUserRoleFactory({ } } ` - const res = await sendRequest(userA.token, { query }) + const res = await sendRequest(tokenUserA, { query }) expect(res).to.be.json expect(res.body.errors).to.not.exist @@ -1752,7 +1706,7 @@ const changeUserRole = changeUserRoleFactory({ } ` - const res2 = await sendRequest(userA.token, { query: query2 }) + const res2 = await sendRequest(tokenUserA, { query: query2 }) expect(res2).to.be.json expect(res2.body.errors).to.not.exist @@ -1760,7 +1714,7 @@ const changeUserRole = changeUserRoleFactory({ }) it('should retrieve a stream commit', async () => { - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: `query { stream( id:"${ts1}" ) { commit( id: "${commitList[0].id}" ) { id message referencedObject } } }` }) @@ -1772,7 +1726,7 @@ const changeUserRole = changeUserRoleFactory({ }) it('should retrieve the latest stream commit if no id is specified', async () => { - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: `query { stream( id:"${ts1}" ) { commit { id message referencedObject } } }` }) expect(res).to.be.json @@ -1795,7 +1749,7 @@ const changeUserRole = changeUserRoleFactory({ it('should save many objects', async () => { const everything = [myCommit, ...myObjs] - const res = await sendRequest(userA.token, { + const res = await sendRequest(tokenUserA, { query: `mutation($objs:[JSONObject]!) { objectCreate(objectInput: {streamId:"${ts1}", objects: $objs}) }`, variables: { objs: everything } }) @@ -1808,7 +1762,7 @@ const changeUserRole = changeUserRoleFactory({ }) it("should get an object's sub-objects' objects", async () => { - const first = await sendRequest(userA.token, { + const first = await sendRequest(tokenUserA, { query: ` query { stream( id:"${ts1}" ) { @@ -1835,7 +1789,7 @@ const changeUserRole = changeUserRoleFactory({ expect(first.body.data.stream.object).to.be.an('object') expect(first.body.data.stream.object.children.objects.length).to.equal(2) - const second = await sendRequest(userA.token, { + const second = await sendRequest(tokenUserA, { query: ` query { stream(id:"${ts1}") { @@ -1870,7 +1824,7 @@ const changeUserRole = changeUserRoleFactory({ }) it("should query an object's subojects", async () => { - const first = await sendRequest(userA.token, { + const first = await sendRequest(tokenUserA, { query: ` query( $query: [JSONObject!], $orderBy: JSONObject ) { stream(id:"${ts1}") { @@ -1999,7 +1953,7 @@ const changeUserRole = changeUserRoleFactory({ info: { name: 'Super Duper Test Server Yo!', company: 'Super Systems' } } - const res = await sendRequest(userA.token, { query, variables }) + const res = await sendRequest(tokenUserA, { query, variables }) expect(res).to.be.json expect(res.body.errors).to.not.exist }) @@ -2011,7 +1965,7 @@ const changeUserRole = changeUserRoleFactory({ info: { name: 'Super Duper Test Server Yo!', company: 'Super Systems' } } - const res = await sendRequest(userB.token, { query, variables }) + const res = await sendRequest(tokenUserB, { query, variables }) expect(res).to.be.json expect(res.body.errors).to.exist expect(res.body.errors[0].extensions.code).to.equal('FORBIDDEN') @@ -2019,17 +1973,17 @@ const changeUserRole = changeUserRoleFactory({ }) describe('Archived role access validation', () => { - const archivedUser = { - id: '', - token: '', - name: 'Mark von Archival', - email: 'archi@speckle.systems', - password: 'i"ll be back, just wait' - } + let archivedUser: BasicTestUser + let archivedUserToken: string let streamId: string before(async () => { - archivedUser.id = await createUser(archivedUser) - archivedUser.token = `Bearer ${await createPersonalAccessToken( + archivedUser = await createTestUser({ + id: '', + name: 'Mark von Archival', + email: 'archi@speckle.systems', + password: 'i"ll be back, just wait' + }) + archivedUserToken = `Bearer ${await createPersonalAccessToken( archivedUser.id, 'this will be archived', [ @@ -2053,7 +2007,7 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should be able to read public streams', async () => { - const streamRes = await sendRequest(userA.token, { + const streamRes = await sendRequest(tokenUserA, { query: 'mutation { streamCreate( stream: { name: "Share this with poor Mark", description: "💩", isPublic:true } ) }' }) @@ -2064,7 +2018,7 @@ const changeUserRole = changeUserRoleFactory({ userA.id ) - const res = await sendRequest(archivedUser.token, { + const res = await sendRequest(archivedUserToken, { query: `query { stream(id:"${streamRes.body.data.streamCreate}") { id name } }` }) expect(res.body.errors).to.not.exist @@ -2074,7 +2028,7 @@ const changeUserRole = changeUserRoleFactory({ it('Should be forbidden to create token', async () => { const query = 'mutation( $tokenInput:ApiTokenCreateInput! ) { apiTokenCreate ( token: $tokenInput ) }' - const res = await sendRequest(archivedUser.token, { + const res = await sendRequest(archivedUserToken, { query, variables: { tokenInput: { @@ -2094,7 +2048,7 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should be forbidden to interact (read, write, delete) private streams it had access to', async () => { - const streamRes = await sendRequest(userA.token, { + const streamRes = await sendRequest(tokenUserA, { query: 'mutation { streamCreate( stream: { name: "Share this with poor Mark", description: "💩", isPublic:false } ) }' }) @@ -2107,7 +2061,7 @@ const changeUserRole = changeUserRoleFactory({ userA.id ) - let res = await sendRequest(archivedUser.token, { + let res = await sendRequest(archivedUserToken, { query: `query { stream(id:"${streamId}") { id name } }` }) expect(res.body.errors).to.exist @@ -2116,7 +2070,7 @@ const changeUserRole = changeUserRoleFactory({ 'You do not have the required server role' ) - res = await sendRequest(archivedUser.token, { + res = await sendRequest(archivedUserToken, { query: '{ user { streams( limit: 30 ) { totalCount cursor items { id name } } } }' }) @@ -2126,7 +2080,7 @@ const changeUserRole = changeUserRoleFactory({ 'You do not have the required server role' ) - res = await sendRequest(archivedUser.token, { + res = await sendRequest(archivedUserToken, { query: `mutation { streamDelete( id:"${streamId}")}` }) expect(res.body.errors).to.exist @@ -2135,7 +2089,7 @@ const changeUserRole = changeUserRoleFactory({ 'You do not have the required server role' ) - res = await sendRequest(archivedUser.token, { + res = await sendRequest(archivedUserToken, { query: `mutation { streamUpdate(stream: {id:"${streamId}" name: "HACK", description: "Hello World, Again!", isPublic:false } ) }` }) expect(res.body.errors).to.exist @@ -2149,7 +2103,7 @@ const changeUserRole = changeUserRoleFactory({ const query = 'mutation ( $streamInput: StreamCreateInput!) { streamCreate(stream: $streamInput ) }' - let res = await sendRequest(archivedUser.token, { + let res = await sendRequest(archivedUserToken, { query, variables: { streamInput: { @@ -2165,7 +2119,7 @@ const changeUserRole = changeUserRoleFactory({ 'You do not have the required server role' ) - res = await sendRequest(archivedUser.token, { + res = await sendRequest(archivedUserToken, { query, variables: { streamInput: { @@ -2195,7 +2149,7 @@ const changeUserRole = changeUserRoleFactory({ } } - const res = await sendRequest(archivedUser.token, { query, variables }) + const res = await sendRequest(archivedUserToken, { query, variables }) expect(res.body.errors).to.exist expect(res.body.errors[0].extensions.code).to.equal('FORBIDDEN') @@ -2205,7 +2159,7 @@ const changeUserRole = changeUserRoleFactory({ }) it('Should be forbidden to send email invites', async () => { - const res = await sendRequest(archivedUser.token, { + const res = await sendRequest(archivedUserToken, { query: 'mutation inviteToServer($input: ServerInviteCreateInput!) { serverInviteCreate( input: $input ) }', variables: { input: { email: 'cabbages@speckle.systems', message: 'wow!' } } @@ -2220,7 +2174,7 @@ const changeUserRole = changeUserRoleFactory({ it('Should be forbidden to create object', async () => { const objects = generateManyObjects(10) - const res = await sendRequest(archivedUser.token, { + const res = await sendRequest(archivedUserToken, { query: `mutation( $objs: [JSONObject]! ) { objectCreate( objectInput: {streamId:"${ts1}", objects: $objs} ) }`, variables: { objs: objects.objs } }) @@ -2239,7 +2193,7 @@ const changeUserRole = changeUserRoleFactory({ objectId: 'justARandomHash', branchName: 'main' } - const res = await sendRequest(archivedUser.token, { + const res = await sendRequest(archivedUserToken, { query: 'mutation( $myCommit: CommitCreateInput! ) { commitCreate( commit: $myCommit ) }', variables: { myCommit: commit } @@ -2255,7 +2209,7 @@ const changeUserRole = changeUserRoleFactory({ const objects = generateManyObjects(2) const res = await request(app) .post(`/objects/${streamId}`) - .set('Authorization', archivedUser.token) + .set('Authorization', archivedUserToken) .set('Content-type', 'multipart/form-data') .attach('batch1', Buffer.from(JSON.stringify(objects.objs), 'utf8')) expect(res).to.have.status(401) @@ -2265,12 +2219,12 @@ const changeUserRole = changeUserRoleFactory({ // even if the object doesn't exist, so im not creating it... const res = await request(app) .get('/objects/thisIs/bogus') - .set('Authorization', archivedUser.token) + .set('Authorization', archivedUserToken) expect(res).to.have.status(401) }) it('Should be able to download from public stream via rest API', async () => { - const streamRes = await sendRequest(userA.token, { + const streamRes = await sendRequest(tokenUserA, { query: 'mutation { streamCreate( stream: { name: "Mark will read this", description: "🥔", isPublic:true } ) }' }) @@ -2285,14 +2239,14 @@ const changeUserRole = changeUserRoleFactory({ const objects = generateManyObjects(2) let res = await request(app) .post(`/objects/${streamRes.body.data.streamCreate}`) - .set('Authorization', userA.token) + .set('Authorization', tokenUserA) .set('Content-type', 'multipart/form-data') .attach('batch1', Buffer.from(JSON.stringify(objects.objs), 'utf8')) expect(res).to.have.status(201) res = await request(app) .get(`/objects/${streamRes.body.data.streamCreate}/${objects.objs[0].id}`) - .set('Authorization', archivedUser.token) + .set('Authorization', archivedUserToken) expect(res).to.have.status(200) expect(res.body[0].id).to.equal(objects.objs[0].id) }) diff --git a/packages/server/modules/core/tests/integration/commits.graph.spec.ts b/packages/server/modules/core/tests/integration/commits.graph.spec.ts index a0f153f25..9b9b7633e 100644 --- a/packages/server/modules/core/tests/integration/commits.graph.spec.ts +++ b/packages/server/modules/core/tests/integration/commits.graph.spec.ts @@ -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() diff --git a/packages/server/modules/core/tests/integration/emailVerification.spec.ts b/packages/server/modules/core/tests/integration/emailVerification.spec.ts index 959995288..a338fc59e 100644 --- a/packages/server/modules/core/tests/integration/emailVerification.spec.ts +++ b/packages/server/modules/core/tests/integration/emailVerification.spec.ts @@ -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() diff --git a/packages/server/modules/core/tests/integration/findUsers.spec.ts b/packages/server/modules/core/tests/integration/findUsers.spec.ts index 3ad2a2305..87e92c820 100644 --- a/packages/server/modules/core/tests/integration/findUsers.spec.ts +++ b/packages/server/modules/core/tests/integration/findUsers.spec.ts @@ -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) }) diff --git a/packages/server/modules/core/tests/integration/objects.graph.spec.ts b/packages/server/modules/core/tests/integration/objects.graph.spec.ts index f0f4bdba8..8e6c3672b 100644 --- a/packages/server/modules/core/tests/integration/objects.graph.spec.ts +++ b/packages/server/modules/core/tests/integration/objects.graph.spec.ts @@ -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() diff --git a/packages/server/modules/core/tests/integration/objects.rest.spec.ts b/packages/server/modules/core/tests/integration/objects.rest.spec.ts index f8378a64a..e3b4f3d99 100644 --- a/packages/server/modules/core/tests/integration/objects.rest.spec.ts +++ b/packages/server/modules/core/tests/integration/objects.rest.spec.ts @@ -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() diff --git a/packages/server/modules/core/tests/integration/objectsStream.rest.spec.ts b/packages/server/modules/core/tests/integration/objectsStream.rest.spec.ts index 01df0fed6..560a1827e 100644 --- a/packages/server/modules/core/tests/integration/objectsStream.rest.spec.ts +++ b/packages/server/modules/core/tests/integration/objectsStream.rest.spec.ts @@ -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() diff --git a/packages/server/modules/core/tests/integration/updateUser.spec.ts b/packages/server/modules/core/tests/integration/updateUser.spec.ts index 1654bae30..5b5dd4da2 100644 --- a/packages/server/modules/core/tests/integration/updateUser.spec.ts +++ b/packages/server/modules/core/tests/integration/updateUser.spec.ts @@ -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()) }) }) diff --git a/packages/server/modules/core/tests/integration/userEmails.graph.spec.ts b/packages/server/modules/core/tests/integration/userEmails.graph.spec.ts index 170909239..9c7ea39da 100644 --- a/packages/server/modules/core/tests/integration/userEmails.graph.spec.ts +++ b/packages/server/modules/core/tests/integration/userEmails.graph.spec.ts @@ -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() diff --git a/packages/server/modules/core/tests/integration/userEmails.spec.ts b/packages/server/modules/core/tests/integration/userEmails.spec.ts index b57662bb3..229906063 100644 --- a/packages/server/modules/core/tests/integration/userEmails.spec.ts +++ b/packages/server/modules/core/tests/integration/userEmails.spec.ts @@ -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) diff --git a/packages/server/modules/core/tests/integration/versions.graph.spec.ts b/packages/server/modules/core/tests/integration/versions.graph.spec.ts index 26292c6bb..d5b609eff 100644 --- a/packages/server/modules/core/tests/integration/versions.graph.spec.ts +++ b/packages/server/modules/core/tests/integration/versions.graph.spec.ts @@ -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 = undefined before(async () => { - user.id = await createUser(user) + user = await createTestUser(buildBasicTestUser()) await createTestWorkspace(workspace, user, { addPlan: { name: 'free', status: 'valid' } }) diff --git a/packages/server/modules/core/tests/objects.spec.ts b/packages/server/modules/core/tests/objects.spec.ts index d59645690..8fd82e288 100644 --- a/packages/server/modules/core/tests/objects.spec.ts +++ b/packages/server/modules/core/tests/objects.spec.ts @@ -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 }) }) diff --git a/packages/server/modules/core/tests/rest.spec.ts b/packages/server/modules/core/tests/rest.spec.ts index fbb1880b6..b862a625a 100644 --- a/packages/server/modules/core/tests/rest.spec.ts +++ b/packages/server/modules/core/tests/rest.spec.ts @@ -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((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((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((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((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) diff --git a/packages/server/modules/core/tests/users.spec.ts b/packages/server/modules/core/tests/users.spec.ts index 286ed20c2..7227e0fc4 100644 --- a/packages/server/modules/core/tests/users.spec.ts +++ b/packages/server/modules/core/tests/users.spec.ts @@ -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', diff --git a/packages/server/modules/core/tests/usersAdmin.spec.ts b/packages/server/modules/core/tests/usersAdmin.spec.ts index 58d773dd9..dd9696d31 100644 --- a/packages/server/modules/core/tests/usersAdmin.spec.ts +++ b/packages/server/modules/core/tests/usersAdmin.spec.ts @@ -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' diff --git a/packages/server/modules/core/tests/usersAdminList.spec.ts b/packages/server/modules/core/tests/usersAdminList.spec.ts index ab55fe9c0..b87daf60b 100644 --- a/packages/server/modules/core/tests/usersAdminList.spec.ts +++ b/packages/server/modules/core/tests/usersAdminList.spec.ts @@ -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(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, - 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, + 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 : '' }`, diff --git a/packages/server/modules/core/tests/usersGraphql.spec.ts b/packages/server/modules/core/tests/usersGraphql.spec.ts index bb67ca623..62b3fae79 100644 --- a/packages/server/modules/core/tests/usersGraphql.spec.ts +++ b/packages/server/modules/core/tests/usersGraphql.spec.ts @@ -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(), diff --git a/packages/server/modules/emails/rest/index.ts b/packages/server/modules/emails/rest/index.ts index ba09fe427..b9a38799b 100644 --- a/packages/server/modules/emails/rest/index.ts +++ b/packages/server/modules/emails/rest/index.ts @@ -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), + return await finalizeEmailVerification(req.query.t as Optional) + }, { logger, - operationName: 'finalizeEmailVerification', - operationDescription: 'Finalize email verification' + dbs: await getAllRegisteredDbs(), + name: 'finalizeEmailVerification', + description: 'Finalize email verification' } ) return res.redirect( diff --git a/packages/server/modules/emails/services/verification/finalize.ts b/packages/server/modules/emails/services/verification/finalize.ts index b8b612938..b05bcf022 100644 --- a/packages/server/modules/emails/services/verification/finalize.ts +++ b/packages/server/modules/emails/services/verification/finalize.ts @@ -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>> 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) + ]) } /** diff --git a/packages/server/modules/fileuploads/tests/helpers/init.ts b/packages/server/modules/fileuploads/tests/helpers/init.ts index 816b0d04a..fddca7756 100644 --- a/packages/server/modules/fileuploads/tests/helpers/init.ts +++ b/packages/server/modules/fileuploads/tests/helpers/init.ts @@ -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, diff --git a/packages/server/modules/fileuploads/tests/integration/fileuploads.spec.ts b/packages/server/modules/fileuploads/tests/integration/fileuploads.spec.ts index 73c56be9f..37aef3b8e 100644 --- a/packages/server/modules/fileuploads/tests/integration/fileuploads.spec.ts +++ b/packages/server/modules/fileuploads/tests/integration/fileuploads.spec.ts @@ -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) diff --git a/packages/server/modules/fileuploads/tests/integration/results.graphql.spec.ts b/packages/server/modules/fileuploads/tests/integration/results.graphql.spec.ts index da1acfd20..6fafd4633 100644 --- a/packages/server/modules/fileuploads/tests/integration/results.graphql.spec.ts +++ b/packages/server/modules/fileuploads/tests/integration/results.graphql.spec.ts @@ -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() diff --git a/packages/server/modules/fileuploads/tests/unit/fileuploads.spec.ts b/packages/server/modules/fileuploads/tests/unit/fileuploads.spec.ts index 205ec47d1..94c6cb303 100644 --- a/packages/server/modules/fileuploads/tests/unit/fileuploads.spec.ts +++ b/packages/server/modules/fileuploads/tests/unit/fileuploads.spec.ts @@ -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 () => { diff --git a/packages/server/modules/multiregion/tests/e2e/serverAdmin.graph.spec.ts b/packages/server/modules/multiregion/tests/e2e/serverAdmin.graph.spec.ts index 818936ea3..915a8674f 100644 --- a/packages/server/modules/multiregion/tests/e2e/serverAdmin.graph.spec.ts +++ b/packages/server/modules/multiregion/tests/e2e/serverAdmin.graph.spec.ts @@ -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', () => { diff --git a/packages/server/modules/multiregion/tests/helpers.ts b/packages/server/modules/multiregion/tests/helpers.ts new file mode 100644 index 000000000..e425a4f09 --- /dev/null +++ b/packages/server/modules/multiregion/tests/helpers.ts @@ -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] +} diff --git a/packages/server/modules/multiregion/tests/integration/repositories/transactions.spec.ts b/packages/server/modules/multiregion/tests/integration/repositories/transactions.spec.ts index e55d71be6..62683530b 100644 --- a/packages/server/modules/multiregion/tests/integration/repositories/transactions.spec.ts +++ b/packages/server/modules/multiregion/tests/integration/repositories/transactions.spec.ts @@ -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' }) diff --git a/packages/server/modules/multiregion/utils/dbSelector.ts b/packages/server/modules/multiregion/utils/dbSelector.ts index a97167b3b..02ed6e04c 100644 --- a/packages/server/modules/multiregion/utils/dbSelector.ts +++ b/packages/server/modules/multiregion/utils/dbSelector.ts @@ -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 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 => { 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 => { 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 +} diff --git a/packages/server/modules/pwdreset/rest/index.ts b/packages/server/modules/pwdreset/rest/index.ts index 861a8bf9b..29c59cc9e 100644 --- a/packages/server/modules/pwdreset/rest/index.ts +++ b/packages/server/modules/pwdreset/rest/index.ts @@ -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` } ) diff --git a/packages/server/modules/pwdreset/tests/pwdrest.spec.ts b/packages/server/modules/pwdreset/tests/pwdrest.spec.ts index fa72cf97f..9d98faf07 100644 --- a/packages/server/modules/pwdreset/tests/pwdrest.spec.ts +++ b/packages/server/modules/pwdreset/tests/pwdrest.spec.ts @@ -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>['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' diff --git a/packages/server/modules/shared/command.ts b/packages/server/modules/shared/command.ts index 8235b7be4..8236c7d39 100644 --- a/packages/server/modules/shared/command.ts +++ b/packages/server/modules/shared/command.ts @@ -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 ( } ) } + +/** + * 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 ( + 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, + 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 => { + 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 + } + ) +} diff --git a/packages/server/modules/shared/domain/constants.ts b/packages/server/modules/shared/domain/constants.ts index 397281db6..c2c3138bb 100644 --- a/packages/server/modules/shared/domain/constants.ts +++ b/packages/server/modules/shared/domain/constants.ts @@ -1,3 +1,8 @@ import { StringEnum } from '@speckle/shared' export const PromiseAllSettledResultStatus = StringEnum(['rejected', 'fulfilled']) + +export const wasRejected = ( + result: PromiseSettledResult +): result is PromiseRejectedResult => + result.status === PromiseAllSettledResultStatus.rejected diff --git a/packages/server/modules/shared/errors/index.ts b/packages/server/modules/shared/errors/index.ts index b01158531..d374585f1 100644 --- a/packages/server/modules/shared/errors/index.ts +++ b/packages/server/modules/shared/errors/index.ts @@ -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) } }) } diff --git a/packages/server/modules/shared/helpers/dbHelper.ts b/packages/server/modules/shared/helpers/dbHelper.ts index 71e2b9681..2b72ffa58 100644 --- a/packages/server/modules/shared/helpers/dbHelper.ts +++ b/packages/server/modules/shared/helpers/dbHelper.ts @@ -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 = { cursor: string | null @@ -312,16 +303,8 @@ export const compositeCursorTools = < } } -export const prepareTransaction = async (db: Knex): Promise => { - 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 => { + await db.raw(`PREPARE TRANSACTION '${gid}';`) } export const commitPreparedTransaction = async ( @@ -337,102 +320,3 @@ export const rollbackPreparedTransaction = async ( ): Promise => { await db.raw(`ROLLBACK PREPARED '${gid}';`) } - -export const replicateQuery = ( - dbs: Knex[], - factory: ({ db }: { db: Knex }) => (params: T) => Promise -) => { - 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 - ) - } - } -} diff --git a/packages/server/modules/shared/test/dbHelper.spec.ts b/packages/server/modules/shared/test/dbHelper.spec.ts index 38f9eadce..8a8d80fb2 100644 --- a/packages/server/modules/shared/test/dbHelper.spec.ts +++ b/packages/server/modules/shared/test/dbHelper.spec.ts @@ -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 diff --git a/packages/server/modules/stats/tests/stats.spec.ts b/packages/server/modules/stats/tests/stats.spec.ts index f0279febf..f670252eb 100644 --- a/packages/server/modules/stats/tests/stats.spec.ts +++ b/packages/server/modules/stats/tests/stats.spec.ts @@ -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>['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> = [] 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 diff --git a/packages/server/modules/webhooks/tests/cleanup.spec.ts b/packages/server/modules/webhooks/tests/cleanup.spec.ts index c6a464614..81621606f 100644 --- a/packages/server/modules/webhooks/tests/cleanup.spec.ts +++ b/packages/server/modules/webhooks/tests/cleanup.spec.ts @@ -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() diff --git a/packages/server/modules/webhooks/tests/webhooks.spec.ts b/packages/server/modules/webhooks/tests/webhooks.spec.ts index e5daf55d3..e8949b71c 100644 --- a/packages/server/modules/webhooks/tests/webhooks.spec.ts +++ b/packages/server/modules/webhooks/tests/webhooks.spec.ts @@ -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>['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 diff --git a/packages/server/modules/workspaces/rest/sso.ts b/packages/server/modules/workspaces/rest/sso.ts index a7b8dfef5..96ea8348f 100644 --- a/packages/server/modules/workspaces/rest/sso.ts +++ b/packages/server/modules/workspaces/rest/sso.ts @@ -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() diff --git a/packages/server/modules/workspaces/tests/integration/workspaceSeat.spec.ts b/packages/server/modules/workspaces/tests/integration/workspaceSeat.spec.ts index 67174e0c2..cbc9b7a6a 100644 --- a/packages/server/modules/workspaces/tests/integration/workspaceSeat.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/workspaceSeat.spec.ts @@ -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: '', diff --git a/packages/server/scripts/seedUsers.ts b/packages/server/scripts/seedUsers.ts index 2f21b6f1e..5a6a60522 100644 --- a/packages/server/scripts/seedUsers.ts +++ b/packages/server/scripts/seedUsers.ts @@ -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[0]> = ( + const userInputs: Array>[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() diff --git a/packages/server/test/authHelper.ts b/packages/server/test/authHelper.ts index 4fdd16283..e6d23e0f8 100644 --- a/packages/server/test/authHelper.ts +++ b/packages/server/test/authHelper.ts @@ -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) { 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): BasicTes id: cryptoRandomString({ length: 10 }), name: cryptoRandomString({ length: 10 }), email: createRandomEmail(), - verified: true + verified: true, + createdAt: new Date(), + suuid: v4() }, overrides )