import { faker } from "@faker-js/faker"; import { v4 as uuidv4 } from "uuid"; import WelcomeEmail from "@server/emails/templates/WelcomeEmail"; import { TeamDomain } from "@server/models"; import Collection from "@server/models/Collection"; import UserAuthentication from "@server/models/UserAuthentication"; import { buildUser, buildTeam, buildAdmin } from "@server/test/factories"; import { setSelfHosted } from "@server/test/support"; import accountProvisioner from "./accountProvisioner"; describe("accountProvisioner", () => { const ip = "127.0.0.1"; describe("hosted", () => { it("should create a new user and team", async () => { const spy = jest.spyOn(WelcomeEmail.prototype, "schedule"); const email = faker.internet.email().toLowerCase(); const { user, team, isNewTeam, isNewUser } = await accountProvisioner({ ip, user: { name: "Jenny Tester", email, avatarUrl: faker.internet.avatar(), }, team: { name: "New workspace", avatarUrl: faker.internet.avatar(), subdomain: faker.internet.domainWord(), }, authenticationProvider: { name: "google", providerId: faker.internet.domainName(), }, authentication: { providerId: uuidv4(), accessToken: "123", scopes: ["read"], }, }); const authentications = await user.$get("authentications"); const auth = authentications[0]; expect(auth.accessToken).toEqual("123"); expect(auth.scopes.length).toEqual(1); expect(auth.scopes[0]).toEqual("read"); expect(team.name).toEqual("New workspace"); expect(user.email).toEqual(email); expect(isNewUser).toEqual(true); expect(isNewTeam).toEqual(true); expect(spy).toHaveBeenCalled(); const collectionCount = await Collection.count({ where: { teamId: team.id, }, }); expect(collectionCount).toEqual(1); spy.mockRestore(); }); it("should update exising user and authentication", async () => { const spy = jest.spyOn(WelcomeEmail.prototype, "schedule"); const existingTeam = await buildTeam(); const providers = await existingTeam.$get("authenticationProviders"); const authenticationProvider = providers[0]; const existing = await buildUser({ teamId: existingTeam.id, }); const authentications = await existing.$get("authentications"); const authentication = authentications[0]; const newEmail = faker.internet.email().toLowerCase(); const { user, isNewUser, isNewTeam } = await accountProvisioner({ ip, user: { name: existing.name, email: newEmail, avatarUrl: existing.avatarUrl, }, team: { name: existingTeam.name, avatarUrl: existingTeam.avatarUrl, subdomain: faker.internet.domainWord(), }, authenticationProvider: { name: authenticationProvider.name, providerId: authenticationProvider.providerId, }, authentication: { providerId: authentication.providerId, accessToken: "123", scopes: ["read"], }, }); const auth = await UserAuthentication.findByPk(authentication.id); expect(auth?.accessToken).toEqual("123"); expect(auth?.scopes.length).toEqual(1); expect(auth?.scopes[0]).toEqual("read"); expect(user.email).toEqual(newEmail); expect(isNewTeam).toEqual(false); expect(isNewUser).toEqual(false); expect(spy).not.toHaveBeenCalled(); spy.mockRestore(); }); it("should allow authentication by email matching", async () => { const subdomain = faker.internet.domainWord(); const existingTeam = await buildTeam({ subdomain, }); const providers = await existingTeam.$get("authenticationProviders"); const authenticationProvider = providers[0]; const email = faker.internet.email().toLowerCase(); const userWithoutAuth = await buildUser({ email, teamId: existingTeam.id, authentications: [], }); const { user, isNewUser, isNewTeam } = await accountProvisioner({ ip, user: { name: userWithoutAuth.name, email, avatarUrl: userWithoutAuth.avatarUrl, }, team: { teamId: existingTeam.id, name: existingTeam.name, avatarUrl: existingTeam.avatarUrl, subdomain, }, authenticationProvider: { name: authenticationProvider.name, providerId: authenticationProvider.providerId, }, authentication: { providerId: uuidv4(), accessToken: "123", scopes: ["read"], }, }); expect(user.id).toEqual(userWithoutAuth.id); expect(isNewTeam).toEqual(false); expect(isNewUser).toEqual(false); expect(user.authentications.length).toEqual(0); }); it("should throw an error when authentication provider is disabled", async () => { const existingTeam = await buildTeam(); const providers = await existingTeam.$get("authenticationProviders"); const authenticationProvider = providers[0]; await authenticationProvider.update({ enabled: false, }); const existing = await buildUser({ teamId: existingTeam.id, }); const authentications = await existing.$get("authentications"); const authentication = authentications[0]; let error; try { await accountProvisioner({ ip, user: { name: existing.name, email: existing.email!, avatarUrl: existing.avatarUrl, }, team: { name: existingTeam.name, avatarUrl: existingTeam.avatarUrl, subdomain: faker.internet.domainWord(), }, authenticationProvider: { name: authenticationProvider.name, providerId: authenticationProvider.providerId, }, authentication: { providerId: authentication.providerId, accessToken: "123", scopes: ["read"], }, }); } catch (err) { error = err; } expect(error).toBeTruthy(); }); it("should throw an error when the domain is not allowed", async () => { const existingTeam = await buildTeam(); const admin = await buildAdmin({ teamId: existingTeam.id }); const providers = await existingTeam.$get("authenticationProviders"); const authenticationProvider = providers[0]; const email = faker.internet.email().toLowerCase(); await TeamDomain.create({ teamId: existingTeam.id, name: "other.com", createdById: admin.id, }); let error; try { await accountProvisioner({ ip, user: { name: "Jenny Tester", email, avatarUrl: faker.internet.avatar(), }, team: { name: existingTeam.name, avatarUrl: existingTeam.avatarUrl, subdomain: faker.internet.domainWord(), }, authenticationProvider: { name: authenticationProvider.name, providerId: authenticationProvider.providerId, }, authentication: { providerId: uuidv4(), accessToken: "123", scopes: ["read"], }, }); } catch (err) { error = err; } expect(error).toBeTruthy(); }); it("should create a new user in an existing team when the domain is allowed", async () => { const spy = jest.spyOn(WelcomeEmail.prototype, "schedule"); const team = await buildTeam(); const admin = await buildAdmin({ teamId: team.id }); const authenticationProviders = await team.$get( "authenticationProviders" ); const authenticationProvider = authenticationProviders[0]; const domain = faker.internet.domainName(); await TeamDomain.create({ teamId: team.id, name: domain, createdById: admin.id, }); const email = faker.internet.email({ provider: domain }); const { user, isNewUser } = await accountProvisioner({ ip, user: { name: "Jenny Tester", email, avatarUrl: faker.internet.avatar(), }, team: { name: team.name, avatarUrl: team.avatarUrl, subdomain: faker.internet.domainWord(), }, authenticationProvider: { name: authenticationProvider.name, providerId: authenticationProvider.providerId, }, authentication: { providerId: uuidv4(), accessToken: "123", scopes: ["read"], }, }); const authentications = await user.$get("authentications"); const auth = authentications[0]; expect(auth.accessToken).toEqual("123"); expect(auth.scopes.length).toEqual(1); expect(auth.scopes[0]).toEqual("read"); expect(user.email).toEqual(email); expect(isNewUser).toEqual(true); expect(spy).toHaveBeenCalled(); // should provision welcome collection const collectionCount = await Collection.count({ where: { teamId: team.id, }, }); expect(collectionCount).toEqual(1); spy.mockRestore(); }); it("should create a new user in an existing team", async () => { const spy = jest.spyOn(WelcomeEmail.prototype, "schedule"); const team = await buildTeam(); const authenticationProviders = await team.$get( "authenticationProviders" ); const authenticationProvider = authenticationProviders[0]; const email = faker.internet.email().toLowerCase(); const { user, isNewUser } = await accountProvisioner({ ip, user: { name: "Jenny Tester", email, avatarUrl: faker.internet.avatar(), }, team: { name: team.name, avatarUrl: team.avatarUrl, subdomain: faker.internet.domainWord(), }, authenticationProvider: { name: authenticationProvider.name, providerId: authenticationProvider.providerId, }, authentication: { providerId: uuidv4(), accessToken: "123", scopes: ["read"], }, }); const authentications = await user.$get("authentications"); const auth = authentications[0]; expect(auth.accessToken).toEqual("123"); expect(auth.scopes.length).toEqual(1); expect(auth.scopes[0]).toEqual("read"); expect(user.email).toEqual(email); expect(isNewUser).toEqual(true); expect(spy).toHaveBeenCalled(); // should provision welcome collection const collectionCount = await Collection.count({ where: { teamId: team.id, }, }); expect(collectionCount).toEqual(1); spy.mockRestore(); }); }); describe.skip("self hosted", () => { beforeEach(setSelfHosted); it("should fail if existing team and domain not in allowed list", async () => { let error; const team = await buildTeam(); try { await accountProvisioner({ ip, user: { name: "Jenny Tester", email: faker.internet.email(), avatarUrl: faker.internet.avatar(), }, team: { teamId: team.id, name: team.name, avatarUrl: team.avatarUrl, subdomain: faker.internet.domainWord(), }, authenticationProvider: { name: "google", providerId: faker.internet.domainName(), }, authentication: { providerId: uuidv4(), accessToken: "123", scopes: ["read"], }, }); } catch (err) { error = err; } expect(error.message).toEqual( "The maximum number of workspaces has been reached" ); }); it("should always use existing team if self-hosted", async () => { const team = await buildTeam(); const domain = faker.internet.domainName(); const { user, isNewUser } = await accountProvisioner({ ip, user: { name: "Jenny Tester", email: faker.internet.email(), avatarUrl: faker.internet.avatar(), }, team: { teamId: team.id, name: team.name, avatarUrl: team.avatarUrl, subdomain: faker.internet.domainWord(), domain, }, authenticationProvider: { name: "google", providerId: domain, }, authentication: { providerId: uuidv4(), accessToken: "123", scopes: ["read"], }, }); expect(user.teamId).toEqual(team.id); expect(isNewUser).toEqual(true); const providers = await team.$get("authenticationProviders"); expect(providers.length).toEqual(2); }); }); });