chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,571 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { describe, expect, vi } from "vitest";
|
||||
import type { PrismaClient } from "@trigger.dev/database";
|
||||
|
||||
const prismaHolder = vi.hoisted(() => ({
|
||||
client: null as PrismaClient | null,
|
||||
}));
|
||||
|
||||
vi.mock("~/services/rbac.server", () => ({
|
||||
rbac: {
|
||||
setUserRole: async () => ({ ok: true as const }),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("~/db.server", () => ({
|
||||
get prisma() {
|
||||
if (!prismaHolder.client) {
|
||||
throw new Error("test prisma not set");
|
||||
}
|
||||
return prismaHolder.client;
|
||||
},
|
||||
get $replica() {
|
||||
if (!prismaHolder.client) {
|
||||
throw new Error("test prisma not set");
|
||||
}
|
||||
return prismaHolder.client;
|
||||
},
|
||||
}));
|
||||
|
||||
import { postgresTest } from "@internal/testcontainers";
|
||||
|
||||
vi.setConfig({ testTimeout: 60_000 });
|
||||
|
||||
function randomHex(len = 12): string {
|
||||
return randomBytes(Math.ceil(len / 2))
|
||||
.toString("hex")
|
||||
.slice(0, len);
|
||||
}
|
||||
|
||||
async function seedInviteFixture(
|
||||
prisma: PrismaClient,
|
||||
opts: { activeProjectCount: number; deletedProjectCount?: number }
|
||||
) {
|
||||
const suffix = randomHex(8);
|
||||
const inviter = await prisma.user.create({
|
||||
data: {
|
||||
email: `inviter-${suffix}@test.local`,
|
||||
authenticationMethod: "MAGIC_LINK",
|
||||
},
|
||||
});
|
||||
const invitee = await prisma.user.create({
|
||||
data: {
|
||||
email: `invitee-${suffix}@test.local`,
|
||||
authenticationMethod: "MAGIC_LINK",
|
||||
},
|
||||
});
|
||||
|
||||
const organization = await prisma.organization.create({
|
||||
data: {
|
||||
title: `invite-org-${suffix}`,
|
||||
slug: `invite-org-${suffix}`,
|
||||
isActivated: true,
|
||||
members: { create: { userId: inviter.id, role: "ADMIN" } },
|
||||
},
|
||||
});
|
||||
|
||||
const activeProjects = [];
|
||||
for (let i = 0; i < opts.activeProjectCount; i++) {
|
||||
activeProjects.push(
|
||||
await prisma.project.create({
|
||||
data: {
|
||||
name: `active-project-${i}-${suffix}`,
|
||||
slug: `active-proj-${i}-${suffix}`,
|
||||
externalRef: `proj_active_${i}_${suffix}`,
|
||||
organizationId: organization.id,
|
||||
engine: "V2",
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const deletedProjectCount = opts.deletedProjectCount ?? 0;
|
||||
for (let i = 0; i < deletedProjectCount; i++) {
|
||||
await prisma.project.create({
|
||||
data: {
|
||||
name: `deleted-project-${i}-${suffix}`,
|
||||
slug: `deleted-proj-${i}-${suffix}`,
|
||||
externalRef: `proj_deleted_${i}_${suffix}`,
|
||||
organizationId: organization.id,
|
||||
engine: "V2",
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const invite = await prisma.orgMemberInvite.create({
|
||||
data: {
|
||||
email: invitee.email,
|
||||
organizationId: organization.id,
|
||||
inviterId: inviter.id,
|
||||
role: "MEMBER",
|
||||
},
|
||||
});
|
||||
|
||||
return { inviter, invitee, organization, activeProjects, invite };
|
||||
}
|
||||
|
||||
function devEnvKeys(apiKey: string, pkApiKey: string) {
|
||||
return { apiKey, pkApiKey, shortcode: randomHex(4) };
|
||||
}
|
||||
|
||||
describe("acceptInvite", () => {
|
||||
postgresTest(
|
||||
"creates member and dev environments for active projects only (many projects)",
|
||||
{ timeout: 60_000 },
|
||||
async ({ prisma }) => {
|
||||
prismaHolder.client = prisma;
|
||||
const { acceptInvite } = await import("../app/models/member.server");
|
||||
|
||||
const { invitee, organization, activeProjects, invite } = await seedInviteFixture(prisma, {
|
||||
activeProjectCount: 25,
|
||||
deletedProjectCount: 3,
|
||||
});
|
||||
|
||||
const beforeEnvCount = await prisma.runtimeEnvironment.count();
|
||||
|
||||
const { organization: joinedOrg } = await acceptInvite({
|
||||
inviteId: invite.id,
|
||||
organizationId: organization.id,
|
||||
user: { id: invitee.id, email: invitee.email },
|
||||
});
|
||||
|
||||
expect(joinedOrg.id).toBe(organization.id);
|
||||
|
||||
const member = await prisma.orgMember.findFirst({
|
||||
where: { userId: invitee.id, organizationId: organization.id },
|
||||
});
|
||||
expect(member).not.toBeNull();
|
||||
|
||||
const devEnvs = await prisma.runtimeEnvironment.findMany({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
orgMemberId: member!.id,
|
||||
type: "DEVELOPMENT",
|
||||
},
|
||||
});
|
||||
expect(devEnvs).toHaveLength(activeProjects.length);
|
||||
|
||||
const envProjectIds = new Set(devEnvs.map((e) => e.projectId));
|
||||
for (const project of activeProjects) {
|
||||
expect(envProjectIds.has(project.id)).toBe(true);
|
||||
}
|
||||
|
||||
const newEnvCount = await prisma.runtimeEnvironment.count();
|
||||
expect(newEnvCount - beforeEnvCount).toBe(activeProjects.length);
|
||||
}
|
||||
);
|
||||
|
||||
postgresTest(
|
||||
"rejects wrong email without creating member or environments",
|
||||
{ timeout: 60_000 },
|
||||
async ({ prisma }) => {
|
||||
prismaHolder.client = prisma;
|
||||
const { acceptInvite, INVITE_NOT_FOUND } = await import("../app/models/member.server");
|
||||
|
||||
const { invitee, organization, invite } = await seedInviteFixture(prisma, {
|
||||
activeProjectCount: 2,
|
||||
});
|
||||
|
||||
const beforeMemberCount = await prisma.orgMember.count({
|
||||
where: { organizationId: organization.id, userId: invitee.id },
|
||||
});
|
||||
const beforeEnvCount = await prisma.runtimeEnvironment.count();
|
||||
|
||||
await expect(
|
||||
acceptInvite({
|
||||
inviteId: invite.id,
|
||||
user: { id: invitee.id, email: "wrong@example.com" },
|
||||
})
|
||||
).rejects.toThrow(INVITE_NOT_FOUND);
|
||||
|
||||
const afterMemberCount = await prisma.orgMember.count({
|
||||
where: { organizationId: organization.id, userId: invitee.id },
|
||||
});
|
||||
expect(afterMemberCount).toBe(beforeMemberCount);
|
||||
|
||||
const afterEnvCount = await prisma.runtimeEnvironment.count();
|
||||
expect(afterEnvCount).toBe(beforeEnvCount);
|
||||
}
|
||||
);
|
||||
|
||||
postgresTest(
|
||||
"rejects invite for deleted organization without creating member or environments",
|
||||
{ timeout: 60_000 },
|
||||
async ({ prisma }) => {
|
||||
prismaHolder.client = prisma;
|
||||
const { acceptInvite, INVITE_NOT_FOUND } = await import("../app/models/member.server");
|
||||
|
||||
const { invitee, organization, invite } = await seedInviteFixture(prisma, {
|
||||
activeProjectCount: 2,
|
||||
});
|
||||
|
||||
await prisma.organization.update({
|
||||
where: { id: organization.id },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
|
||||
const beforeMemberCount = await prisma.orgMember.count({
|
||||
where: { organizationId: organization.id, userId: invitee.id },
|
||||
});
|
||||
const beforeEnvCount = await prisma.runtimeEnvironment.count();
|
||||
|
||||
await expect(
|
||||
acceptInvite({
|
||||
inviteId: invite.id,
|
||||
user: { id: invitee.id, email: invitee.email },
|
||||
})
|
||||
).rejects.toThrow(INVITE_NOT_FOUND);
|
||||
|
||||
const afterMemberCount = await prisma.orgMember.count({
|
||||
where: { organizationId: organization.id, userId: invitee.id },
|
||||
});
|
||||
expect(afterMemberCount).toBe(beforeMemberCount);
|
||||
|
||||
const afterEnvCount = await prisma.runtimeEnvironment.count();
|
||||
expect(afterEnvCount).toBe(beforeEnvCount);
|
||||
}
|
||||
);
|
||||
|
||||
postgresTest(
|
||||
"rejects already consumed invite with normalized error",
|
||||
{ timeout: 60_000 },
|
||||
async ({ prisma }) => {
|
||||
prismaHolder.client = prisma;
|
||||
const { acceptInvite, INVITE_NOT_FOUND } = await import("../app/models/member.server");
|
||||
|
||||
const { invitee, organization, invite } = await seedInviteFixture(prisma, {
|
||||
activeProjectCount: 1,
|
||||
});
|
||||
|
||||
await prisma.orgMemberInvite.delete({ where: { id: invite.id } });
|
||||
|
||||
await expect(
|
||||
acceptInvite({
|
||||
inviteId: invite.id,
|
||||
user: { id: invitee.id, email: invitee.email },
|
||||
})
|
||||
).rejects.toThrow(INVITE_NOT_FOUND);
|
||||
|
||||
const member = await prisma.orgMember.findFirst({
|
||||
where: { userId: invitee.id, organizationId: organization.id },
|
||||
});
|
||||
expect(member).toBeNull();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe("provisionMemberDevelopmentEnvironments", () => {
|
||||
postgresTest(
|
||||
"skips projects that already have development environments",
|
||||
{ timeout: 60_000 },
|
||||
async ({ prisma }) => {
|
||||
prismaHolder.client = prisma;
|
||||
const { provisionMemberDevelopmentEnvironments } =
|
||||
await import("../app/models/member.server");
|
||||
|
||||
const { invitee, organization, activeProjects, invite } = await seedInviteFixture(prisma, {
|
||||
activeProjectCount: 3,
|
||||
});
|
||||
|
||||
await prisma.orgMemberInvite.delete({ where: { id: invite.id } });
|
||||
|
||||
const member = await prisma.orgMember.create({
|
||||
data: {
|
||||
organizationId: organization.id,
|
||||
userId: invitee.id,
|
||||
role: "MEMBER",
|
||||
},
|
||||
});
|
||||
|
||||
const keys = devEnvKeys(`tr_dev_${randomHex(24)}`, `pk_dev_${randomHex(24)}`);
|
||||
await prisma.runtimeEnvironment.create({
|
||||
data: {
|
||||
slug: "dev",
|
||||
type: "DEVELOPMENT",
|
||||
...keys,
|
||||
projectId: activeProjects[1].id,
|
||||
organizationId: organization.id,
|
||||
orgMemberId: member.id,
|
||||
},
|
||||
});
|
||||
|
||||
await provisionMemberDevelopmentEnvironments({
|
||||
inviteId: invite.id,
|
||||
user: { id: invitee.id, email: invitee.email },
|
||||
member,
|
||||
organization,
|
||||
projects: activeProjects,
|
||||
maximumConcurrencyLimit: 5,
|
||||
});
|
||||
|
||||
const devEnvs = await prisma.runtimeEnvironment.findMany({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
orgMemberId: member.id,
|
||||
type: "DEVELOPMENT",
|
||||
},
|
||||
});
|
||||
|
||||
expect(devEnvs).toHaveLength(activeProjects.length);
|
||||
}
|
||||
);
|
||||
|
||||
postgresTest(
|
||||
"throws partial-success error when env creation fails mid-loop",
|
||||
{ timeout: 60_000 },
|
||||
async ({ prisma }) => {
|
||||
prismaHolder.client = prisma;
|
||||
const { provisionMemberDevelopmentEnvironments, ENV_SETUP_INCOMPLETE } =
|
||||
await import("../app/models/member.server");
|
||||
|
||||
const { invitee, organization, activeProjects, invite } = await seedInviteFixture(prisma, {
|
||||
activeProjectCount: 2,
|
||||
});
|
||||
|
||||
await prisma.orgMemberInvite.delete({ where: { id: invite.id } });
|
||||
|
||||
const member = await prisma.orgMember.create({
|
||||
data: {
|
||||
organizationId: organization.id,
|
||||
userId: invitee.id,
|
||||
role: "MEMBER",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
provisionMemberDevelopmentEnvironments({
|
||||
inviteId: invite.id,
|
||||
user: { id: invitee.id, email: invitee.email },
|
||||
member,
|
||||
organization,
|
||||
projects: [...activeProjects, { id: "missing-project-id" }],
|
||||
maximumConcurrencyLimit: 5,
|
||||
})
|
||||
).rejects.toThrow(ENV_SETUP_INCOMPLETE);
|
||||
|
||||
const devEnvs = await prisma.runtimeEnvironment.findMany({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
orgMemberId: member.id,
|
||||
type: "DEVELOPMENT",
|
||||
},
|
||||
});
|
||||
|
||||
const envProjectIds = devEnvs.map((env) => env.projectId);
|
||||
expect(envProjectIds).toContain(activeProjects[0].id);
|
||||
expect(envProjectIds).toContain(activeProjects[1].id);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe("acceptInvite recovery", () => {
|
||||
postgresTest(
|
||||
"retries successfully when membership exists and the invite is still pending",
|
||||
{ timeout: 60_000 },
|
||||
async ({ prisma }) => {
|
||||
prismaHolder.client = prisma;
|
||||
const { acceptInvite } = await import("../app/models/member.server");
|
||||
|
||||
const { invitee, organization, activeProjects, invite } = await seedInviteFixture(prisma, {
|
||||
activeProjectCount: 3,
|
||||
});
|
||||
|
||||
const member = await prisma.orgMember.create({
|
||||
data: {
|
||||
organizationId: organization.id,
|
||||
userId: invitee.id,
|
||||
role: "MEMBER",
|
||||
},
|
||||
});
|
||||
|
||||
const keys = devEnvKeys(`tr_dev_${randomHex(24)}`, `pk_dev_${randomHex(24)}`);
|
||||
await prisma.runtimeEnvironment.create({
|
||||
data: {
|
||||
slug: "dev",
|
||||
type: "DEVELOPMENT",
|
||||
...keys,
|
||||
projectId: activeProjects[0].id,
|
||||
organizationId: organization.id,
|
||||
orgMemberId: member.id,
|
||||
},
|
||||
});
|
||||
|
||||
const { organization: joinedOrg } = await acceptInvite({
|
||||
inviteId: invite.id,
|
||||
organizationId: organization.id,
|
||||
user: { id: invitee.id, email: invitee.email },
|
||||
});
|
||||
|
||||
expect(joinedOrg.id).toBe(organization.id);
|
||||
|
||||
const devEnvs = await prisma.runtimeEnvironment.findMany({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
orgMemberId: member.id,
|
||||
type: "DEVELOPMENT",
|
||||
},
|
||||
});
|
||||
expect(devEnvs).toHaveLength(activeProjects.length);
|
||||
|
||||
const remainingInvite = await prisma.orgMemberInvite.findFirst({
|
||||
where: { id: invite.id },
|
||||
});
|
||||
expect(remainingInvite).toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
postgresTest(
|
||||
"recovers when the invite was already consumed but development environments are incomplete",
|
||||
{ timeout: 60_000 },
|
||||
async ({ prisma }) => {
|
||||
prismaHolder.client = prisma;
|
||||
const { acceptInvite } = await import("../app/models/member.server");
|
||||
|
||||
const { invitee, organization, activeProjects, invite } = await seedInviteFixture(prisma, {
|
||||
activeProjectCount: 3,
|
||||
});
|
||||
|
||||
await prisma.orgMemberInvite.delete({ where: { id: invite.id } });
|
||||
|
||||
const member = await prisma.orgMember.create({
|
||||
data: {
|
||||
organizationId: organization.id,
|
||||
userId: invitee.id,
|
||||
role: "MEMBER",
|
||||
},
|
||||
});
|
||||
|
||||
const keys = devEnvKeys(`tr_dev_${randomHex(24)}`, `pk_dev_${randomHex(24)}`);
|
||||
await prisma.runtimeEnvironment.create({
|
||||
data: {
|
||||
slug: "dev",
|
||||
type: "DEVELOPMENT",
|
||||
...keys,
|
||||
projectId: activeProjects[0].id,
|
||||
organizationId: organization.id,
|
||||
orgMemberId: member.id,
|
||||
},
|
||||
});
|
||||
|
||||
const { organization: joinedOrg } = await acceptInvite({
|
||||
inviteId: invite.id,
|
||||
organizationId: organization.id,
|
||||
user: { id: invitee.id, email: invitee.email },
|
||||
});
|
||||
|
||||
expect(joinedOrg.id).toBe(organization.id);
|
||||
|
||||
const devEnvs = await prisma.runtimeEnvironment.findMany({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
orgMemberId: member.id,
|
||||
type: "DEVELOPMENT",
|
||||
},
|
||||
});
|
||||
expect(devEnvs).toHaveLength(activeProjects.length);
|
||||
}
|
||||
);
|
||||
|
||||
postgresTest(
|
||||
"does not recover unrelated memberships when invite is missing and organizationId is omitted",
|
||||
{ timeout: 60_000 },
|
||||
async ({ prisma }) => {
|
||||
prismaHolder.client = prisma;
|
||||
const { acceptInvite, INVITE_NOT_FOUND } = await import("../app/models/member.server");
|
||||
|
||||
const fixtureA = await seedInviteFixture(prisma, { activeProjectCount: 2 });
|
||||
const fixtureB = await seedInviteFixture(prisma, { activeProjectCount: 2 });
|
||||
|
||||
await prisma.orgMemberInvite.delete({ where: { id: fixtureA.invite.id } });
|
||||
|
||||
const member = await prisma.orgMember.create({
|
||||
data: {
|
||||
organizationId: fixtureA.organization.id,
|
||||
userId: fixtureA.invitee.id,
|
||||
role: "MEMBER",
|
||||
},
|
||||
});
|
||||
|
||||
const keys = devEnvKeys(`tr_dev_${randomHex(24)}`, `pk_dev_${randomHex(24)}`);
|
||||
await prisma.runtimeEnvironment.create({
|
||||
data: {
|
||||
slug: "dev",
|
||||
type: "DEVELOPMENT",
|
||||
...keys,
|
||||
projectId: fixtureA.activeProjects[0].id,
|
||||
organizationId: fixtureA.organization.id,
|
||||
orgMemberId: member.id,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
acceptInvite({
|
||||
inviteId: fixtureA.invite.id,
|
||||
user: { id: fixtureA.invitee.id, email: fixtureA.invitee.email },
|
||||
})
|
||||
).rejects.toThrow(INVITE_NOT_FOUND);
|
||||
|
||||
const devEnvs = await prisma.runtimeEnvironment.findMany({
|
||||
where: {
|
||||
organizationId: fixtureA.organization.id,
|
||||
orgMemberId: member.id,
|
||||
type: "DEVELOPMENT",
|
||||
},
|
||||
});
|
||||
expect(devEnvs).toHaveLength(1);
|
||||
expect(fixtureB.invite.id).not.toBe(fixtureA.invite.id);
|
||||
}
|
||||
);
|
||||
|
||||
postgresTest(
|
||||
"does not recover memberships for a different organizationId than the stale invite",
|
||||
{ timeout: 60_000 },
|
||||
async ({ prisma }) => {
|
||||
prismaHolder.client = prisma;
|
||||
const { acceptInvite, INVITE_NOT_FOUND } = await import("../app/models/member.server");
|
||||
|
||||
const fixtureA = await seedInviteFixture(prisma, { activeProjectCount: 2 });
|
||||
const fixtureB = await seedInviteFixture(prisma, { activeProjectCount: 2 });
|
||||
|
||||
await prisma.orgMemberInvite.delete({ where: { id: fixtureA.invite.id } });
|
||||
|
||||
const member = await prisma.orgMember.create({
|
||||
data: {
|
||||
organizationId: fixtureA.organization.id,
|
||||
userId: fixtureA.invitee.id,
|
||||
role: "MEMBER",
|
||||
},
|
||||
});
|
||||
|
||||
const keys = devEnvKeys(`tr_dev_${randomHex(24)}`, `pk_dev_${randomHex(24)}`);
|
||||
await prisma.runtimeEnvironment.create({
|
||||
data: {
|
||||
slug: "dev",
|
||||
type: "DEVELOPMENT",
|
||||
...keys,
|
||||
projectId: fixtureA.activeProjects[0].id,
|
||||
organizationId: fixtureA.organization.id,
|
||||
orgMemberId: member.id,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
acceptInvite({
|
||||
inviteId: fixtureA.invite.id,
|
||||
organizationId: fixtureB.organization.id,
|
||||
user: { id: fixtureA.invitee.id, email: fixtureA.invitee.email },
|
||||
})
|
||||
).rejects.toThrow(INVITE_NOT_FOUND);
|
||||
|
||||
const devEnvs = await prisma.runtimeEnvironment.findMany({
|
||||
where: {
|
||||
organizationId: fixtureA.organization.id,
|
||||
orgMemberId: member.id,
|
||||
type: "DEVELOPMENT",
|
||||
},
|
||||
});
|
||||
expect(devEnvs).toHaveLength(1);
|
||||
}
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user