Files
calendar/apps/web/lib/orgMigration.test.ts
T
12466fb064 feat: New Organization Schema along with the ability to invite existing users to an organization (#13002)
* Add profile table

Copy to profile table

Update schema

Migrations

* wip

* Get switcher working

* Add organization seeder

* wip

* Remove dead code

* Fix unit tests

* relevantProfile -> profile

* cache bust

* One more comment

* Squash migrations

* Copy to profile table

* Update schema

* Fix existing user invite flow

* Add profile table

* Copy to profile table

* Update schema

* Format

* Add profile table

Copy to profile table

Update schema

Migrations

* Format

* wip

Get switcher working

Add organization seeder

wip

Remove dead code

Fix unit tests

relevantProfile -> profile

cache bust

One more comment

Squash migrations

Copy to profile table

Update schema

Fix existing user invite flow

Add profile table

Copy to profile table

Update schema

chore: small improvements to signup (#13227)

* removed features on mobile, better background color

* nit

* dark mode nits

New Crowdin translations by Github Action

fix: event reschedule workflow trigger (#13122)

* fix event reschedule workflow trigger

* uncomment previously failed test

* review fix

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>

chore: added community support to help (#13213)

refactor: Abstract `handleSeats` from `handleNewBooking` (#12062)

* Refactor createBooking

* Type fix

* Abstract handleSeats

* Create Invitee type

* Create OrganizerUser type

* Abstract addVideoCallDataToEvt

* Abstract createLoggerWithEventDetails

* Abstract `handleAppStatus` from handler

* Create ReqAppsStatus type

* Move `deleteMeeting` and `getCalendar`

* Set parameters for `handleSeats`

* Typescript refactor

* Change function params from req

* Type fix

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Erik <erik@erosemberg.com>

fix: Dynamic group booking  org-migration redirect (#13223)

* fix: Dynamic group booking redirect

* Fix flaky private team test

test: Check availability tab and all your functions (teste2e-availability) (#12434)

* add changes

* add changes

* add changes

* add changes

* Requested changes

* Change way to grab element

---------

Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

fix: Reschedule a cancelled booking (#13170)

* fix: Reschedule a cancelled booking

* canceled instead of cancelled

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

v3.7.0

test: In Team Event type when location is Organizer Default App and U… (#12939)

* test: In Team Event type when location is Organizer Default App and User Metadata is emoty

* chore: use variable

* fix: type error

test: add test for invalid cal video (#13103)

* test: add test for invalid cal video

* chore: add confirm

* chore: fix type error

* chore: update code

* test: add unit test

* chore: type error

* chore

* chore: type

* chore: fix test

* chore

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

chore: add error message for no availability (#13230)

* chore: add error message for no default user availability

* chore: check only availability

* chore: change message

* chore: add eventType

fix: Team revert with members (#13228)

refactor: Refactor seats logic (#12905)

* Refactor createBooking

* Type fix

* Abstract handleSeats

* Create Invitee type

* Create OrganizerUser type

* Abstract addVideoCallDataToEvt

* Abstract createLoggerWithEventDetails

* Abstract `handleAppStatus` from handler

* Create ReqAppsStatus type

* Move `deleteMeeting` and `getCalendar`

* Set parameters for `handleSeats`

* Typescript refactor

* Change function params from req

* Type fix

* Move handleSeats

* Abstract lastAttendeeDeleteBooking

* Create function for rescheduling seated events

* Fix imports on reschedule seats function

* Fix imports

* Import handleSeats function

* Fix rescheduleUid type

* Refactor owner reschedule to new time slot

* Refactor combine two booking times together

* Reschedule as an attendee

* Refactor createNewSeat

* Remove old handleSeats

* Remove lastAttendeeDeleteBooking from handleNewBooking

* Test for new attendee right params are passed

* Unit test params for reschedule

* Typo fix

* Clean up

* Create new seat test

* Test when attendee already signs up for booking

* Type fix

* Test reschedule move attendee to existing booking

* On reschedule create new booking

* Test on last attendee cancel booking

* Owner reschedule to new time slot

* Owner rescheduling, merge two bookings together

* Test: when merging more than available seats, then fail

* Test: fail when event is full

* Remove duplicate E2E tests

* Clean up

* Rename `addVideoCallDataToEvt` to `addVideoCallDataToEvent`

* Refactor `calcAppsStatus`

* Assign `evt` to resutl of `addVideoCallDataToEvent`

* Use prisma.transaction when moving attendees

* Clean create seat call

* Use ErrorCode enum

* Use attendeeRescheduledSeatedBooking function

* Await function

* Prevent double triggering of workflows

* Use inviteeToAdd in createNewSeat

* Remove unused error code

* Remove old handleSeats file

* Type fix

* Type fix

* Type fix

* Type fix

* Type fix

* Type fix

* Type fix

* Type fix

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Erik <erik@erosemberg.com>

feat: Allow admins to filter bookings only by attendee emails (#13236)

* chore: Allow admins to filter bookings only by attendee emails

* Fix type error

Fixed [CAL-2930] Org branding isnt centered in shell #13225 (#13237)

Add profile table

Copy to profile table

Update schema

Migrations

Format

* wip

* Add profile relation everywhere

* Fix schema

* Fix invite handler

* Add profileId during eventTypec reation

* Get dashboard working with profile relatioon

* Add workflow create repository

* Add profileId to host

* Add profileId to credential table

* Add destinationCalendar repository

* Add profileId to membership

* Revert all profile related changes

* Add acme and dunder

* Fix membership profileId usgae

* Various fixes

* Some renaming

* Fix failing tests

* Debugging next-aut

* Consolidate migrations

* Testing fixes

* Fix unit tests

* Self review feedback

* PR feedback

* Fix orgMigration as well

* Add profile schema and copy users to it

* Remove migrations from here

* Fix type issues

* Fix type error after main merge

* Remove movedFromUserId

* Add missing migration file

* Self review addressed

* Joe's review addressed

* Fix profile switcher

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-02-02 21:34:13 +00:00

1677 lines
44 KiB
TypeScript

import prismock from "../../../tests/libs/__mocks__/prisma";
import { describe, expect, it } from "vitest";
import type { z } from "zod";
import { WEBSITE_URL } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import type { MembershipRole, Prisma } from "@calcom/prisma/client";
import { RedirectType } from "@calcom/prisma/enums";
import type { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { moveTeamToOrg, moveUserToOrg, removeTeamFromOrg, removeUserFromOrg } from "./orgMigration";
const WEBSITE_PROTOCOL = new URL(WEBSITE_URL).protocol;
describe("orgMigration", () => {
describe("moveUserToOrg", () => {
describe("when user email does not match orgAutoAcceptEmail", () => {
it(`should migrate a user to become a part of an organization with ADMIN role
- username in the organization should be automatically derived from email when it isn't passed`, async () => {
const data = {
userToMigrate: {
username: "user-1",
email: "user-1@example.com",
// Because example.com isn't the orgAutoAcceptEmail
expectedUsernameInOrg: "user-1-example",
},
targetOrg: {
name: "Org 1",
slug: "org1",
},
membershipWeWant: {
role: "ADMIN",
} as const,
};
const dbUserToMigrate = await createUserOutsideOrg({
email: data.userToMigrate.email,
username: data.userToMigrate.username,
});
const dbOrg = await createOrg({
name: data.targetOrg.name,
slug: data.targetOrg.slug,
});
const team1 = await createTeamOutsideOrg({
name: "Team-1",
slug: "team-1",
});
// Make userToMigrate part of team-1
await addMemberShipOfUserWithTeam({
teamId: team1.id,
userId: dbUserToMigrate.id,
role: "MEMBER",
accepted: true,
});
await moveUserToOrg({
user: {
id: dbUserToMigrate.id,
},
targetOrg: {
id: dbOrg.id,
membership: {
role: data.membershipWeWant.role,
},
},
shouldMoveTeams: false,
});
await expectUserToBeAPartOfOrg({
userId: dbUserToMigrate.id,
orgId: dbOrg.id,
usernameInOrg: data.userToMigrate.expectedUsernameInOrg,
expectedMembership: { role: data.membershipWeWant.role, accepted: true },
});
await expectTeamToBeNotPartOfAnyOrganization({
teamId: team1.id,
});
expectUserRedirectToBeEnabled({
from: {
username: data.userToMigrate.username,
},
to: data.userToMigrate.expectedUsernameInOrg,
orgSlug: data.targetOrg.slug,
});
});
it("should migrate a user to become a part of an organization(which has slug set) with MEMBER role", async () => {
const data = {
userToMigrate: {
username: "user-1",
email: "user-1@example.com",
// Because example.com isn't the orgAutoAcceptEmail
expectedUsernameInOrg: "user-1-example",
},
targetOrg: {
id: 1,
name: "Org 1",
slug: "org1",
},
membershipWeWant: {
role: "MEMBER",
} as const,
};
const dbUserToMigrate = await createUserOutsideOrg({
email: data.userToMigrate.email,
username: data.userToMigrate.username,
});
const dbOrg = await createOrg({
slug: data.targetOrg.slug,
name: data.targetOrg.name,
});
await moveUserToOrg({
user: {
id: dbOrg.id,
},
targetOrg: {
id: data.targetOrg.id,
membership: {
role: data.membershipWeWant.role,
},
},
shouldMoveTeams: false,
});
await expectUserToBeAPartOfOrg({
userId: dbUserToMigrate.id,
orgId: data.targetOrg.id,
usernameInOrg: data.userToMigrate.expectedUsernameInOrg,
expectedMembership: { role: data.membershipWeWant.role, accepted: true },
});
});
it(`should migrate a user to become a part of an organization(which has no slug but requestedSlug) with MEMBER role
1. Should set the slug as requestedSlug for the organization(so that the redirect doesnt take to an unpublished organization page)`, async () => {
const data = {
userToMigrate: {
username: "user-1",
email: "user-1@example.com",
// Because example.com isn't the orgAutoAcceptEmail
expectedUsernameInOrg: "user-1-example",
},
targetOrg: {
name: "Org 1",
requestedSlug: "org1",
},
membershipWeWant: {
role: "MEMBER",
} as const,
};
const dbUserToMigrate = await createUserOutsideOrg({
email: "user-1@example.com",
username: "user-1",
});
const dbOrg = await createOrg({
name: data.targetOrg.name,
metadata: {
requestedSlug: data.targetOrg.requestedSlug,
},
});
await moveUserToOrg({
user: {
id: dbUserToMigrate.id,
},
targetOrg: {
id: dbOrg.id,
membership: {
role: data.membershipWeWant.role,
},
},
shouldMoveTeams: false,
});
const organization = await prismock.team.findUnique({
where: {
id: dbOrg.id,
},
});
expect(organization?.slug).toBe(data.targetOrg.requestedSlug);
await expectUserToBeAPartOfOrg({
userId: dbUserToMigrate.id,
orgId: dbOrg.id,
usernameInOrg: data.userToMigrate.expectedUsernameInOrg,
expectedMembership: { role: data.membershipWeWant.role, accepted: true },
});
});
it("should migrate a user along with its teams(without other members)", async () => {
const data = {
userToMigrate: {
username: "user-1",
email: "user-1@example.com",
// Because example.com isn't the orgAutoAcceptEmail
expectedUsernameInOrg: "user-1-example",
teams: [
{
name: "Team 100",
slug: "team-100",
},
{
name: "Team 101",
slug: "team-101",
},
],
},
targetOrg: {
name: "Org 1",
slug: "org1",
},
membershipWeWant: {
role: "MEMBER",
} as const,
userPartOfTeam1: {
username: "user-2",
email: "user-2@example.com",
},
userPartOfTeam2: {
username: "user-3",
email: "user-3@example.com",
},
};
// Create user to migrate
const dbUserToMigrate = await createUserOutsideOrg({
email: data.userToMigrate.email,
username: data.userToMigrate.username,
});
// Create another user that would be part of team-1 along with userToMigrate
const userPartOfTeam1 = await createUserOutsideOrg({
email: data.userPartOfTeam1.email,
username: data.userPartOfTeam1.username,
});
// Create another user that would be part of team-2 along with userToMigrate
const userPartOfTeam2 = await createUserOutsideOrg({
email: data.userPartOfTeam2.email,
username: data.userPartOfTeam2.username,
});
const team1 = await createTeamOutsideOrg({
name: data.userToMigrate.teams[0].name,
slug: data.userToMigrate.teams[0].slug,
});
const team2 = await createTeamOutsideOrg({
name: data.userToMigrate.teams[1].name,
slug: data.userToMigrate.teams[1].slug,
});
// Make userToMigrate part of team-1
await addMemberShipOfUserWithTeam({
teamId: team1.id,
userId: dbUserToMigrate.id,
role: "MEMBER",
accepted: true,
});
// Make userToMigrate part of team-2
await addMemberShipOfUserWithTeam({
teamId: team2.id,
userId: dbUserToMigrate.id,
role: "MEMBER",
accepted: true,
});
// Make userPartofTeam1 part of team-1
await addMemberShipOfUserWithTeam({
teamId: team1.id,
userId: userPartOfTeam1.id,
role: "MEMBER",
accepted: true,
});
// Make userPartofTeam2 part of team2
await addMemberShipOfUserWithTeam({
teamId: team2.id,
userId: userPartOfTeam2.id,
role: "MEMBER",
accepted: true,
});
const dbOrg = await createOrg({
name: data.targetOrg.name,
slug: data.targetOrg.slug,
});
await moveUserToOrg({
user: {
id: dbUserToMigrate.id,
},
targetOrg: {
id: dbOrg.id,
membership: {
role: data.membershipWeWant.role,
},
},
shouldMoveTeams: true,
});
await expectUserToBeAPartOfOrg({
userId: dbUserToMigrate.id,
orgId: dbOrg.id,
usernameInOrg: data.userToMigrate.expectedUsernameInOrg,
expectedMembership: { role: data.membershipWeWant.role, accepted: true },
});
await expectTeamToBeAPartOfOrg({
teamId: team1.id,
orgId: dbOrg.id,
teamSlugInOrg: team1.slug,
});
await expectTeamToBeAPartOfOrg({
teamId: team2.id,
orgId: dbOrg.id,
teamSlugInOrg: team2.slug,
});
await expectUserToBeNotAPartOfTheOrg({
userId: userPartOfTeam1.id,
orgId: dbOrg.id,
username: data.userPartOfTeam1.username,
});
await expectUserToBeNotAPartOfTheOrg({
userId: userPartOfTeam2.id,
orgId: dbOrg.id,
username: data.userPartOfTeam2.username,
});
});
it(`should migrate a user to become a part of an organization
- username in the organization should same as provided to moveUserToOrg`, async () => {
const data = {
userToMigrate: {
username: "user-1",
email: "user-1@example.com",
usernameInOrgThatWeWant: "user-1-in-org",
},
targetOrg: {
name: "Org 1",
slug: "org1",
},
membershipWeWant: {
role: "ADMIN",
} as const,
};
const dbUserToMigrate = await createUserOutsideOrg({
email: data.userToMigrate.email,
username: data.userToMigrate.username,
});
const dbOrg = await createOrg({
name: data.targetOrg.name,
slug: data.targetOrg.slug,
});
await moveUserToOrg({
user: {
id: dbUserToMigrate.id,
},
targetOrg: {
id: dbOrg.id,
username: data.userToMigrate.usernameInOrgThatWeWant,
membership: {
role: data.membershipWeWant.role,
},
},
shouldMoveTeams: false,
});
await expectUserToBeAPartOfOrg({
userId: dbUserToMigrate.id,
orgId: dbOrg.id,
usernameInOrg: data.userToMigrate.usernameInOrgThatWeWant,
expectedMembership: { role: data.membershipWeWant.role, accepted: true },
});
expectUserRedirectToBeEnabled({
from: {
username: data.userToMigrate.username,
},
to: data.userToMigrate.usernameInOrgThatWeWant,
orgSlug: data.targetOrg.slug,
});
});
it(`should be able to re-migrate an already migrated user fixing things as we do it
- Redirect should correctly determine the nonOrgUsername so that on repeat migrations, the original user link doesn't break`, async () => {
const data = {
userToMigrate: {
username: "user-1",
email: "user-1@example.com",
// Because example.com isn't the orgAutoAcceptEmail
usernameWeWantInOrg: "user-1-in-org",
},
targetOrg: {
name: "Org 1",
slug: "org1",
},
membershipWeWant: {
role: "MEMBER",
} as const,
};
const dbOrg = await createOrg({
name: data.targetOrg.name,
slug: data.targetOrg.slug,
});
const dbUserToMigrate = await createUserInsideTheOrg(
{
email: data.userToMigrate.email,
username: data.userToMigrate.username,
},
dbOrg.id
);
await moveUserToOrg({
user: {
id: dbUserToMigrate.id,
},
targetOrg: {
id: dbOrg.id,
username: data.userToMigrate.usernameWeWantInOrg,
membership: {
role: data.membershipWeWant.role,
},
},
shouldMoveTeams: false,
});
await expectUserToBeAPartOfOrg({
userId: dbUserToMigrate.id,
orgId: dbOrg.id,
usernameInOrg: data.userToMigrate.usernameWeWantInOrg,
// membership role should be updated
expectedMembership: { role: data.membershipWeWant.role, accepted: true },
});
await moveUserToOrg({
user: {
id: dbUserToMigrate.id,
},
targetOrg: {
id: dbOrg.id,
username: data.userToMigrate.usernameWeWantInOrg,
membership: {
role: data.membershipWeWant.role,
},
},
shouldMoveTeams: false,
});
await expectUserToBeAPartOfOrg({
userId: dbUserToMigrate.id,
orgId: dbOrg.id,
usernameInOrg: data.userToMigrate.usernameWeWantInOrg,
// membership role should be updated
expectedMembership: { role: data.membershipWeWant.role, accepted: true },
});
expectUserRedirectToBeEnabled({
from: {
username: data.userToMigrate.username,
},
to: data.userToMigrate.usernameWeWantInOrg,
orgSlug: data.targetOrg.slug,
});
console.log(await prismock.tempOrgRedirect.findMany({}));
});
describe("Failures handling", () => {
it("migration should fail if the username already exists in the organization", async () => {
const data = {
userToMigrate: {
username: "user-1",
email: "user-1@example.com",
// Because example.com isn't the orgAutoAcceptEmail
expectedUsernameInOrg: "user-1-example",
},
targetOrg: {
name: "Org 1",
slug: "org1",
},
membershipWeWant: {
role: "MEMBER",
} as const,
};
const dbOrg = await createOrg({
name: data.targetOrg.name,
slug: data.targetOrg.slug,
});
await createUserInsideTheOrg(
{
email: data.userToMigrate.email,
username: data.userToMigrate.username,
},
dbOrg.id
);
// User with same username exists outside the org as well as inside the org
const dbUserToMigrate = await createUserOutsideOrg({
email: data.userToMigrate.email,
username: data.userToMigrate.username,
});
expect(() => {
return moveUserToOrg({
user: {
id: dbUserToMigrate.id,
},
targetOrg: {
id: dbOrg.id,
username: data.userToMigrate.username,
membership: {
role: data.membershipWeWant.role,
},
},
shouldMoveTeams: false,
});
}).rejects.toThrowError("already exists");
});
it("should fail the migration if the target org doesn't exist", async () => {
const data = {
userToMigrate: {
username: "user-1",
email: "user-1@example.com",
// Because example.com isn't the orgAutoAcceptEmail
expectedUsernameInOrg: "user-1-example",
},
targetOrg: {
name: "Org 1",
slug: "org1",
},
membershipWeWant: {
role: "MEMBER",
} as const,
};
const dbOrg = await createOrg({
name: data.targetOrg.name,
slug: data.targetOrg.slug,
});
await createUserInsideTheOrg(
{
email: data.userToMigrate.email,
username: data.userToMigrate.username,
},
dbOrg.id
);
// User with same username exists outside the org as well as inside the org
const dbUserToMigrate = await createUserOutsideOrg({
email: data.userToMigrate.email,
username: data.userToMigrate.username,
});
expect(() => {
return moveUserToOrg({
user: {
id: dbUserToMigrate.id,
},
targetOrg: {
// Non existent teamId
id: 1001,
username: data.userToMigrate.username,
membership: {
role: data.membershipWeWant.role,
},
},
shouldMoveTeams: false,
});
}).rejects.toThrowError(/Org .* not found/);
});
it("should fail the migration if the target teamId is not an organization", async () => {
const data = {
userToMigrate: {
username: "user-1",
email: "user-1@example.com",
// Because example.com isn't the orgAutoAcceptEmail
expectedUsernameInOrg: "user-1-example",
},
targetOrg: {
name: "Org 1",
slug: "org1",
},
membershipWeWant: {
role: "MEMBER",
} as const,
};
// Create a team instead of an organization
const dbOrgWhichIsActuallyATeam = await createTeamOutsideOrg({
name: data.targetOrg.name,
slug: data.targetOrg.slug,
});
const dbUserToMigrate = await createUserOutsideOrg({
email: data.userToMigrate.email,
username: data.userToMigrate.username,
});
expect(() => {
return moveUserToOrg({
user: {
id: dbUserToMigrate.id,
},
targetOrg: {
// Non existent teamId
id: dbOrgWhichIsActuallyATeam.id,
username: data.userToMigrate.username,
membership: {
role: data.membershipWeWant.role,
},
},
shouldMoveTeams: false,
});
}).rejects.toThrowError(/is not an Org/);
});
it("should fail the migration if the user is part of any other organization", async () => {
const data = {
userToMigrate: {
username: "user-1",
email: "user-1@example.com",
// Because example.com isn't the orgAutoAcceptEmail
expectedUsernameInOrg: "user-1-example",
},
targetOrg: {
name: "Org 1",
slug: "org1",
},
membershipWeWant: {
role: "MEMBER",
} as const,
};
const dbOrg = await createOrg({
name: data.targetOrg.name,
slug: data.targetOrg.slug,
});
const dbOrgOther = await createOrg({
name: data.targetOrg.name,
slug: data.targetOrg.slug,
});
const dbUserToMigrate = await createUserInsideTheOrg(
{
email: data.userToMigrate.email,
username: data.userToMigrate.username,
},
dbOrgOther.id
);
expect(() => {
return moveUserToOrg({
user: {
id: dbUserToMigrate.id,
},
targetOrg: {
// Non existent teamId
id: dbOrg.id,
username: data.userToMigrate.username,
membership: {
role: data.membershipWeWant.role,
},
},
shouldMoveTeams: false,
});
}).rejects.toThrowError(/already a part of different organization/);
});
});
});
describe("when user email matches orgAutoAcceptEmail", () => {
const orgMetadata = {
orgAutoAcceptEmail: "org1.com",
} as const;
it(`should migrate a user to become a part of an organization with ADMIN role`, async () => {
const data = {
userToMigrate: {
username: "user-1",
email: "user-1@org1.com",
// Because example.com isn't the orgAutoAcceptEmail
expectedUsernameInOrg: "user-1",
},
targetOrg: {
name: "Org 1",
slug: "org1",
},
membershipWeWant: {
role: "ADMIN",
} as const,
};
const dbUserToMigrate = await createUserOutsideOrg({
email: data.userToMigrate.email,
username: data.userToMigrate.username,
});
const dbOrg = await createOrg({
slug: data.targetOrg.slug,
name: data.targetOrg.name,
metadata: {
...orgMetadata,
},
});
await moveUserToOrg({
user: {
id: dbUserToMigrate.id,
},
targetOrg: {
id: dbOrg.id,
membership: {
role: data.membershipWeWant.role,
},
},
shouldMoveTeams: false,
});
await expectUserToBeAPartOfOrg({
userId: dbUserToMigrate.id,
orgId: dbOrg.id,
usernameInOrg: data.userToMigrate.expectedUsernameInOrg,
expectedMembership: { role: data.membershipWeWant.role, accepted: true },
});
expectUserRedirectToBeEnabled({
from: {
username: data.userToMigrate.username,
},
to: data.userToMigrate.expectedUsernameInOrg,
orgSlug: data.targetOrg.slug,
});
});
it("should migrate a user to become a part of an organization(which has slug set) with MEMBER role", async () => {
const data = {
userToMigrate: {
username: "user-1",
email: "user-1@org1.com",
// Because example.com isn't the orgAutoAcceptEmail
expectedUsernameInOrg: "user-1",
},
targetOrg: {
name: "Org 1",
slug: "org1",
},
membershipWeWant: {
role: "MEMBER",
} as const,
};
const dbUserToMigrate = await createUserOutsideOrg({
email: data.userToMigrate.email,
username: data.userToMigrate.username,
});
const dbOrg = await createOrg({
slug: data.targetOrg.slug,
name: data.targetOrg.name,
metadata: {
...orgMetadata,
},
});
await moveUserToOrg({
user: {
id: dbUserToMigrate.id,
},
targetOrg: {
id: dbOrg.id,
membership: {
role: data.membershipWeWant.role,
},
},
shouldMoveTeams: false,
});
await expectUserToBeAPartOfOrg({
userId: dbUserToMigrate.id,
orgId: dbOrg.id,
usernameInOrg: data.userToMigrate.expectedUsernameInOrg,
expectedMembership: { role: data.membershipWeWant.role, accepted: true },
});
});
it(`should migrate a user to become a part of an organization(which has no slug but requestedSlug) with MEMBER role
1. Should set the slug as requestedSlug for the organization(so that the redirect doesnt take to an unpublished organization page)`, async () => {
const data = {
userToMigrate: {
username: "user-1",
email: "user-1@org1.com",
// Because example.com isn't the orgAutoAcceptEmail
expectedUsernameInOrg: "user-1",
},
targetOrg: {
name: "Org 1",
requestedSlug: "org1",
},
membershipWeWant: {
role: "MEMBER",
} as const,
};
const dbUserToMigrate = await createUserOutsideOrg({
email: data.userToMigrate.email,
username: data.userToMigrate.username,
});
const dbOrg = await createOrg({
name: data.targetOrg.name,
metadata: {
requestedSlug: data.targetOrg.requestedSlug,
...orgMetadata,
},
});
await moveUserToOrg({
user: {
id: dbUserToMigrate.id,
},
targetOrg: {
id: dbOrg.id,
membership: {
role: data.membershipWeWant.role,
},
},
shouldMoveTeams: false,
});
const organization = await prismock.team.findUnique({
where: {
id: dbOrg.id,
},
});
expect(organization?.slug).toBe(data.targetOrg.requestedSlug);
await expectUserToBeAPartOfOrg({
userId: dbUserToMigrate.id,
orgId: dbOrg.id,
usernameInOrg: data.userToMigrate.expectedUsernameInOrg,
expectedMembership: { role: data.membershipWeWant.role, accepted: true },
});
});
});
});
describe("moveTeamToOrg", () => {
it(`should migrate a team to become a part of an organization`, async () => {
const data = {
teamToMigrate: {
id: 1,
name: "Team 1",
slug: "team1",
newSlug: "team1-new-slug",
},
targetOrg: {
id: 2,
name: "Org 1",
slug: "org1",
},
};
await prismock.team.create({
data: {
id: data.teamToMigrate.id,
slug: data.teamToMigrate.slug,
name: data.teamToMigrate.name,
},
});
await prismock.team.create({
data: {
id: data.targetOrg.id,
slug: data.targetOrg.slug,
name: data.targetOrg.name,
metadata: {
isOrganization: true,
},
},
});
await moveTeamToOrg({
teamId: data.teamToMigrate.id,
targetOrg: {
id: data.targetOrg.id,
teamSlug: data.teamToMigrate.newSlug,
},
});
await expectTeamToBeAPartOfOrg({
teamId: data.teamToMigrate.id,
orgId: data.targetOrg.id,
teamSlugInOrg: data.teamToMigrate.newSlug,
});
expectTeamRedirectToBeEnabled({
from: {
teamSlug: data.teamToMigrate.slug,
},
to: data.teamToMigrate.newSlug,
orgSlug: data.targetOrg.slug,
});
});
it.todo("should migrate a team with members");
it.todo("Try migrating an already migrated team");
});
describe("removeUserFromOrg", () => {
it(`should remove a user from an organization but he should remain in team`, async () => {
const data = {
userToUnmigrate: {
username: "user1-in-org1",
email: "user-1@example.com",
usernameBeforeMovingToOrg: "user1",
},
targetOrg: {
name: "Org 1",
slug: "org1",
},
membershipWeWant: {
role: "ADMIN",
} as const,
};
const dbOrg = await createOrg({
slug: data.targetOrg.slug,
name: data.targetOrg.name,
});
const dbTeamOutsideOrg = await createTeamOutsideOrg({
slug: "team-1",
name: "Team 1",
});
const dbUser = await createUserInsideTheOrg(
{
email: data.userToUnmigrate.email,
username: data.userToUnmigrate.username,
metadata: {
migratedToOrgFrom: {
username: data.userToUnmigrate.usernameBeforeMovingToOrg,
},
},
},
dbOrg.id
);
await addMemberShipOfUserWithOrg({
userId: dbUser.id,
teamId: dbTeamOutsideOrg.id,
role: "MEMBER",
accepted: true,
});
await addMemberShipOfUserWithTeam({
userId: dbUser.id,
teamId: dbOrg.id,
role: data.membershipWeWant.role,
accepted: true,
});
const userToUnmigrate = await prismock.user.findUnique({
where: {
id: dbUser.id,
},
include: {
organization: true,
},
});
if (!userToUnmigrate?.organizationId || !userToUnmigrate.organization) {
throw new Error(
`Couldn't setup user to unmigrate properly userToUnMigrate: ${{
organizationId: userToUnmigrate?.organizationId,
organization: !!userToUnmigrate?.organization,
}}`
);
}
await removeUserFromOrg({
userId: dbUser.id,
targetOrgId: dbOrg.id,
});
await expectUserToBeNotAPartOfTheOrg({
userId: dbUser.id,
orgId: dbOrg.id,
username: data.userToUnmigrate.usernameBeforeMovingToOrg,
});
await expectUserToBeAPartOfTeam({
userId: dbUser.id,
teamId: dbTeamOutsideOrg.id,
expectedMembership: {
role: "MEMBER",
accepted: true,
},
});
expectUserRedirectToBeNotEnabled({
from: {
username: data.userToUnmigrate.username,
},
});
});
});
describe("removeTeamFromOrg", () => {
it(`should remove a team from an organization`, async () => {
const data = {
teamToUnmigrate: {
name: "Team 1",
slug: "team1",
},
targetOrg: {
name: "Org 1",
slug: "org1",
},
};
const targetOrg = await prismock.team.create({
data: {
slug: data.targetOrg.slug,
name: data.targetOrg.name,
metadata: {
isOrganization: true,
},
},
});
const { id: teamToUnMigrateId } = await prismock.team.create({
data: {
slug: data.teamToUnmigrate.slug,
name: data.teamToUnmigrate.name,
parent: {
connect: {
id: targetOrg.id,
},
},
},
});
const teamToUnmigrate = await prismock.team.findUnique({
where: {
id: teamToUnMigrateId,
},
include: {
parent: true,
},
});
if (!teamToUnmigrate?.parent || !teamToUnmigrate.parentId) {
throw new Error(`Couldn't setup team to unmigrate properly ID:${teamToUnMigrateId}`);
}
await removeTeamFromOrg({
teamId: teamToUnMigrateId,
targetOrgId: targetOrg.id,
});
await expectTeamToBeNotPartOfAnyOrganization({
teamId: teamToUnMigrateId,
});
expectTeamRedirectToBeNotEnabled({
from: {
teamSlug: data.teamToUnmigrate.slug,
},
to: data.teamToUnmigrate.slug,
orgSlug: data.targetOrg.slug,
});
});
it(`should remove a team from an organization with it's members`, async () => {
const data = {
teamToUnmigrate: {
name: "Team 1",
slug: "team1",
members: [
{
username: "user1-example",
email: "user1@example.com",
usernameBeforeMovingToOrg: "user1",
},
],
},
targetOrg: {
name: "Org 1",
slug: "org1",
},
};
const targetOrg = await prismock.team.create({
data: {
slug: data.targetOrg.slug,
name: data.targetOrg.name,
metadata: {
isOrganization: true,
},
},
});
const { id: teamToUnMigrateId } = await prismock.team.create({
data: {
slug: data.teamToUnmigrate.slug,
name: data.teamToUnmigrate.name,
parent: {
connect: {
id: targetOrg.id,
},
},
},
});
const teamToUnmigrate = await prismock.team.findUnique({
where: {
id: teamToUnMigrateId,
},
include: {
parent: true,
},
});
if (!teamToUnmigrate?.parent || !teamToUnmigrate.parentId) {
throw new Error(`Couldn't setup team to unmigrate properly ID:${teamToUnMigrateId}`);
}
const member1OfTeam = await createUserInsideTheOrg(
{
email: data.teamToUnmigrate.members[0].email,
username: data.teamToUnmigrate.members[0].username,
metadata: {
migratedToOrgFrom: {
username: data.teamToUnmigrate.members[0].usernameBeforeMovingToOrg,
},
},
},
targetOrg.id
);
addMemberShipOfUserWithOrg({
userId: member1OfTeam.id,
teamId: targetOrg.id,
role: "MEMBER",
accepted: true,
});
addMemberShipOfUserWithTeam({
teamId: teamToUnmigrate.id,
userId: member1OfTeam.id,
role: "MEMBER",
accepted: true,
});
expectUserToBeAPartOfOrg({
userId: member1OfTeam.id,
orgId: targetOrg.id,
usernameInOrg: data.teamToUnmigrate.members[0].username,
expectedMembership: {
role: "MEMBER",
accepted: true,
},
});
expectUserToBeAPartOfTeam({
userId: member1OfTeam.id,
teamId: teamToUnmigrate.id,
expectedMembership: {
role: "MEMBER",
accepted: true,
},
});
await removeTeamFromOrg({
teamId: teamToUnMigrateId,
targetOrgId: targetOrg.id,
});
await expectTeamToBeNotPartOfAnyOrganization({
teamId: teamToUnMigrateId,
});
expectTeamRedirectToBeNotEnabled({
from: {
teamSlug: data.teamToUnmigrate.slug,
},
to: data.teamToUnmigrate.slug,
orgSlug: data.targetOrg.slug,
});
expectUserRedirectToBeNotEnabled({
from: {
username: data.teamToUnmigrate.members[0].usernameBeforeMovingToOrg,
},
});
expectUserToBeNotAPartOfTheOrg({
userId: member1OfTeam.id,
orgId: targetOrg.id,
username: data.teamToUnmigrate.members[0].usernameBeforeMovingToOrg,
});
expectUserToBeNotAPartOfTheTeam({
userId: member1OfTeam.id,
teamId: targetOrg.id,
username: data.teamToUnmigrate.members[0].usernameBeforeMovingToOrg,
});
});
});
});
async function expectUserToBeAPartOfOrg({
userId,
orgId,
expectedMembership,
usernameInOrg,
}: {
userId: number;
orgId: number;
usernameInOrg: string;
expectedMembership: {
role: MembershipRole;
accepted: boolean;
};
}) {
const migratedUser = await prismock.user.findUnique({
where: {
id: userId,
},
include: {
teams: true,
},
});
if (!migratedUser) {
throw new Error(`User with id ${userId} does not exist`);
}
expect(migratedUser.username).toBe(usernameInOrg);
expect(migratedUser.organizationId).toBe(orgId);
const membership = migratedUser.teams.find(
(membership) => membership.teamId === orgId && membership.userId === userId
);
expect(membership).not.toBeNull();
if (!membership) {
throw new Error(`User with id ${userId} is not a part of org with id ${orgId}`);
}
expect(membership.role).toBe(expectedMembership.role);
expect(membership.accepted).toBe(expectedMembership.accepted);
}
async function expectUserToBeAPartOfTeam({
userId,
teamId,
expectedMembership,
}: {
userId: number;
teamId: number;
expectedMembership: {
role: MembershipRole;
accepted: boolean;
};
}) {
const user = await prismock.user.findUnique({
where: {
id: userId,
},
include: {
teams: true,
},
});
if (!user) {
throw new Error(`User with id ${userId} does not exist`);
}
const membership = user.teams.find(
(membership) => membership.teamId === teamId && membership.userId === userId
);
expect(membership).not.toBeNull();
if (!membership) {
throw new Error(`User with id ${userId} is not a part of team with id ${teamId}`);
}
expect(membership.role).toBe(expectedMembership.role);
expect(membership.accepted).toBe(expectedMembership.accepted);
}
async function expectUserToBeNotAPartOfTheOrg({
userId,
orgId,
username,
}: {
userId: number;
orgId: number;
username: string;
}) {
expectUserToBeNotAPartOfTheTeam({
userId,
teamId: orgId,
username,
});
}
async function expectUserToBeNotAPartOfTheTeam({
userId,
teamId,
username,
}: {
userId: number;
teamId: number;
username: string;
}) {
const user = await prismock.user.findUnique({
where: {
id: userId,
},
include: {
teams: true,
},
});
if (!user) {
throw new Error(`User with id ${userId} does not exist`);
}
expect(user.username).toBe(username);
expect(user.organizationId).toBe(null);
const membership = user.teams.find(
(membership) => membership.teamId === teamId && membership.userId === userId
);
expect(membership).toBeUndefined();
}
async function expectTeamToBeAPartOfOrg({
teamId,
orgId,
teamSlugInOrg,
}: {
teamId: number;
orgId: number;
teamSlugInOrg: string | null;
}) {
const migratedTeam = await prismock.team.findUnique({
where: {
id: teamId,
},
});
if (!migratedTeam) {
throw new Error(`Team with id ${teamId} does not exist`);
}
if (!teamSlugInOrg) {
throw new Error(`teamSlugInOrg should be defined`);
}
expect(migratedTeam.parentId).toBe(orgId);
expect(migratedTeam.slug).toBe(teamSlugInOrg);
}
async function expectTeamToBeNotPartOfAnyOrganization({ teamId }: { teamId: number }) {
const team = await prismock.team.findUnique({
where: {
id: teamId,
},
});
if (!team) {
throw new Error(`Team with id ${teamId} does not exist`);
}
expect(team.parentId).toBe(null);
}
async function expectUserRedirectToBeEnabled({
from,
to,
orgSlug,
}: {
from: { username: string } | { teamSlug: string };
to: string;
orgSlug: string;
}) {
expectRedirectToBeEnabled({
from,
to,
orgSlug,
redirectType: RedirectType.User,
});
}
async function expectTeamRedirectToBeEnabled({
from,
to,
orgSlug,
}: {
from: { username: string } | { teamSlug: string };
to: string;
orgSlug: string;
}) {
expectRedirectToBeEnabled({
from,
to,
orgSlug,
redirectType: RedirectType.Team,
});
}
async function expectUserRedirectToBeNotEnabled({
from,
}: {
from: { username: string } | { teamSlug: string };
}) {
expectRedirectToBeNotEnabled({
from,
});
}
async function expectTeamRedirectToBeNotEnabled({
from,
}: {
from: { username: string } | { teamSlug: string };
to: string;
orgSlug: string;
}) {
expectRedirectToBeNotEnabled({
from,
});
}
async function expectRedirectToBeEnabled({
from,
to,
orgSlug,
redirectType,
}: {
from: { username: string } | { teamSlug: string };
to: string;
orgSlug: string;
redirectType: RedirectType;
}) {
let tempOrgRedirectWhere = null;
let tempOrgRedirectThatShouldNotExistWhere = null;
if ("username" in from) {
tempOrgRedirectWhere = {
from_type_fromOrgId: {
from: from.username,
type: RedirectType.User,
fromOrgId: 0,
},
};
// Normally with user migration `from.username != to`
if (from.username !== to) {
// There must not be a redirect setup from=To to something else as that would cause double redirection
tempOrgRedirectThatShouldNotExistWhere = {
from_type_fromOrgId: {
from: to,
type: RedirectType.User,
fromOrgId: 0,
},
};
}
} else if ("teamSlug" in from) {
tempOrgRedirectWhere = {
from_type_fromOrgId: {
from: from.teamSlug,
type: RedirectType.Team,
fromOrgId: 0,
},
};
if (from.teamSlug !== to) {
// There must not be a redirect setup from=To to something else as that would cause double redirection
tempOrgRedirectThatShouldNotExistWhere = {
from_type_fromOrgId: {
from: to,
type: RedirectType.Team,
fromOrgId: 0,
},
};
}
} else {
throw new Error("Atleast one of userId or teamId should be present in from");
}
const redirect = await prismock.tempOrgRedirect.findUnique({
where: tempOrgRedirectWhere,
});
if (tempOrgRedirectThatShouldNotExistWhere) {
const redirectThatShouldntBeThere = await prismock.tempOrgRedirect.findUnique({
where: tempOrgRedirectThatShouldNotExistWhere,
});
expect(redirectThatShouldntBeThere).toBeNull();
}
expect(redirect).not.toBeNull();
expect(redirect?.toUrl).toBe(`${WEBSITE_PROTOCOL}//${orgSlug}.cal.local:3000/${to}`);
if (!redirect) {
throw new Error(`Redirect doesn't exist for ${JSON.stringify(tempOrgRedirectWhere)}`);
}
expect(redirect.type).toBe(redirectType);
}
async function expectRedirectToBeNotEnabled({ from }: { from: { username: string } | { teamSlug: string } }) {
let tempOrgRedirectWhere = null;
if ("username" in from) {
tempOrgRedirectWhere = {
from_type_fromOrgId: {
from: from.username,
type: RedirectType.User,
fromOrgId: 0,
},
};
} else if ("teamSlug" in from) {
tempOrgRedirectWhere = {
from_type_fromOrgId: {
from: from.teamSlug,
type: RedirectType.Team,
fromOrgId: 0,
},
};
} else {
throw new Error("Atleast one of userId or teamId should be present in from");
}
const redirect = await prismock.tempOrgRedirect.findUnique({
where: tempOrgRedirectWhere,
});
expect(redirect).toBeNull();
}
async function createOrg(
data: Omit<Prisma.TeamCreateArgs["data"], "metadata" | "parent"> & {
metadata?: z.infer<typeof teamMetadataSchema>;
}
) {
return await prismock.team.create({
data: {
...data,
metadata: {
...(data.metadata || {}),
isOrganization: true,
},
},
});
}
async function createTeamOutsideOrg(
data: Omit<Prisma.TeamCreateArgs["data"], "metadata" | "parent"> & {
metadata?: z.infer<typeof teamMetadataSchema>;
}
) {
return await prismock.team.create({
data: {
...data,
parentId: null,
metadata: {
...(data.metadata || {}),
isOrganization: false,
},
},
});
}
async function createUserOutsideOrg(
data: Omit<Prisma.UserCreateArgs["data"], "organization" | "movedToProfile">
) {
return await prismock.user.create({
data: {
...data,
movedToProfileId: null,
organizationId: null,
},
});
}
async function createUserInsideTheOrg(
data: Omit<Prisma.UserUncheckedCreateInput, "organization" | "organizationId" | "id" | "movedToProfileId">,
orgId: number
) {
const org = await prismock.team.findUnique({
where: {
id: orgId,
},
});
if (!org) {
throw new Error(`Org with id ${orgId} does not exist`);
}
logger.debug(
`Creating user inside org`,
safeStringify({
orgId,
data,
})
);
return await prismock.user.create({
data: {
...data,
organization: {
connect: {
id: orgId,
},
},
},
});
}
async function addMemberShipOfUserWithTeam({
teamId,
userId,
role,
accepted,
}: {
teamId: number;
userId: number;
role: MembershipRole;
accepted: boolean;
}) {
await prismock.membership.create({
data: {
role,
accepted,
team: {
connect: {
id: teamId,
},
},
user: {
connect: {
id: userId,
},
},
},
});
const membership = await prismock.membership.findUnique({
where: {
userId_teamId: {
teamId,
userId,
},
},
});
if (!membership) {
throw new Error(`Membership between teamId ${teamId} and userId ${userId} couldn't be created`);
}
}
const addMemberShipOfUserWithOrg = addMemberShipOfUserWithTeam;