Compare commits

...

2 Commits

Author SHA1 Message Date
Piyush Gupta
a59dc8c109 fix: backport in organization landing (#6049) 2025-06-23 17:53:09 +05:30
Piyush Gupta
975a3a2157 fix: backport in organization landing 2025-06-23 09:59:06 +05:30
4 changed files with 197 additions and 26 deletions

View File

@@ -8,7 +8,7 @@ import { TUser } from "@formbricks/types/user";
import Page from "./page"; import Page from "./page";
vi.mock("@/lib/project/service", () => ({ vi.mock("@/lib/project/service", () => ({
getProjectEnvironmentsByOrganizationIds: vi.fn(), getUserProjectEnvironmentsByOrganizationIds: vi.fn(),
})); }));
vi.mock("@/lib/instance/service", () => ({ vi.mock("@/lib/instance/service", () => ({
@@ -152,7 +152,7 @@ describe("Page", () => {
const { getIsFreshInstance } = await import("@/lib/instance/service"); const { getIsFreshInstance } = await import("@/lib/instance/service");
const { getUser } = await import("@/lib/user/service"); const { getUser } = await import("@/lib/user/service");
const { getOrganizationsByUserId } = await import("@/lib/organization/service"); const { getOrganizationsByUserId } = await import("@/lib/organization/service");
const { getProjectEnvironmentsByOrganizationIds } = await import("@/lib/project/service"); const { getUserProjectEnvironmentsByOrganizationIds } = await import("@/lib/project/service");
const { getMembershipByUserIdOrganizationId } = await import("@/lib/membership/service"); const { getMembershipByUserIdOrganizationId } = await import("@/lib/membership/service");
const { getAccessFlags } = await import("@/lib/membership/utils"); const { getAccessFlags } = await import("@/lib/membership/utils");
const { redirect } = await import("next/navigation"); const { redirect } = await import("next/navigation");
@@ -220,7 +220,7 @@ describe("Page", () => {
} as any); } as any);
vi.mocked(getIsFreshInstance).mockResolvedValue(false); vi.mocked(getIsFreshInstance).mockResolvedValue(false);
vi.mocked(getUser).mockResolvedValue(mockUser); vi.mocked(getUser).mockResolvedValue(mockUser);
vi.mocked(getProjectEnvironmentsByOrganizationIds).mockResolvedValue( vi.mocked(getUserProjectEnvironmentsByOrganizationIds).mockResolvedValue(
mockUserProjects as unknown as TProject[] mockUserProjects as unknown as TProject[]
); );
vi.mocked(getOrganizationsByUserId).mockResolvedValue([mockOrganization]); vi.mocked(getOrganizationsByUserId).mockResolvedValue([mockOrganization]);
@@ -241,7 +241,7 @@ describe("Page", () => {
const { getServerSession } = await import("next-auth"); const { getServerSession } = await import("next-auth");
const { getIsFreshInstance } = await import("@/lib/instance/service"); const { getIsFreshInstance } = await import("@/lib/instance/service");
const { getUser } = await import("@/lib/user/service"); const { getUser } = await import("@/lib/user/service");
const { getProjectEnvironmentsByOrganizationIds } = await import("@/lib/project/service"); const { getUserProjectEnvironmentsByOrganizationIds } = await import("@/lib/project/service");
const { getOrganizationsByUserId } = await import("@/lib/organization/service"); const { getOrganizationsByUserId } = await import("@/lib/organization/service");
const { getMembershipByUserIdOrganizationId } = await import("@/lib/membership/service"); const { getMembershipByUserIdOrganizationId } = await import("@/lib/membership/service");
const { getAccessFlags } = await import("@/lib/membership/utils"); const { getAccessFlags } = await import("@/lib/membership/utils");
@@ -310,7 +310,7 @@ describe("Page", () => {
} as any); } as any);
vi.mocked(getIsFreshInstance).mockResolvedValue(false); vi.mocked(getIsFreshInstance).mockResolvedValue(false);
vi.mocked(getUser).mockResolvedValue(mockUser); vi.mocked(getUser).mockResolvedValue(mockUser);
vi.mocked(getProjectEnvironmentsByOrganizationIds).mockResolvedValue( vi.mocked(getUserProjectEnvironmentsByOrganizationIds).mockResolvedValue(
mockUserProjects as unknown as TProject[] mockUserProjects as unknown as TProject[]
); );
vi.mocked(getOrganizationsByUserId).mockResolvedValue([mockOrganization]); vi.mocked(getOrganizationsByUserId).mockResolvedValue([mockOrganization]);
@@ -334,7 +334,7 @@ describe("Page", () => {
const { getOrganizationsByUserId } = await import("@/lib/organization/service"); const { getOrganizationsByUserId } = await import("@/lib/organization/service");
const { getMembershipByUserIdOrganizationId } = await import("@/lib/membership/service"); const { getMembershipByUserIdOrganizationId } = await import("@/lib/membership/service");
const { getAccessFlags } = await import("@/lib/membership/utils"); const { getAccessFlags } = await import("@/lib/membership/utils");
const { getProjectEnvironmentsByOrganizationIds } = await import("@/lib/project/service"); const { getUserProjectEnvironmentsByOrganizationIds } = await import("@/lib/project/service");
const { render } = await import("@testing-library/react"); const { render } = await import("@testing-library/react");
const mockUser: TUser = { const mockUser: TUser = {
@@ -432,7 +432,7 @@ describe("Page", () => {
vi.mocked(getUser).mockResolvedValue(mockUser); vi.mocked(getUser).mockResolvedValue(mockUser);
vi.mocked(getOrganizationsByUserId).mockResolvedValue([mockOrganization]); vi.mocked(getOrganizationsByUserId).mockResolvedValue([mockOrganization]);
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership); vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership);
vi.mocked(getProjectEnvironmentsByOrganizationIds).mockResolvedValue(mockUserProjects); vi.mocked(getUserProjectEnvironmentsByOrganizationIds).mockResolvedValue(mockUserProjects);
vi.mocked(getAccessFlags).mockReturnValue({ vi.mocked(getAccessFlags).mockReturnValue({
isManager: false, isManager: false,
isOwner: false, isOwner: false,

View File

@@ -3,7 +3,7 @@ import { getIsFreshInstance } from "@/lib/instance/service";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service"; import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getAccessFlags } from "@/lib/membership/utils"; import { getAccessFlags } from "@/lib/membership/utils";
import { getOrganizationsByUserId } from "@/lib/organization/service"; import { getOrganizationsByUserId } from "@/lib/organization/service";
import { getProjectEnvironmentsByOrganizationIds } from "@/lib/project/service"; import { getUserProjectEnvironmentsByOrganizationIds } from "@/lib/project/service";
import { getUser } from "@/lib/user/service"; import { getUser } from "@/lib/user/service";
import { authOptions } from "@/modules/auth/lib/authOptions"; import { authOptions } from "@/modules/auth/lib/authOptions";
import { ClientLogout } from "@/modules/ui/components/client-logout"; import { ClientLogout } from "@/modules/ui/components/client-logout";
@@ -34,7 +34,10 @@ const Page = async () => {
return redirect("/setup/organization/create"); return redirect("/setup/organization/create");
} }
const projectsByOrg = await getProjectEnvironmentsByOrganizationIds(userOrganizations.map((org) => org.id)); const projectsByOrg = await getUserProjectEnvironmentsByOrganizationIds(
userOrganizations.map((org) => org.id),
user.id
);
// Flatten all environments from all projects across all organizations // Flatten all environments from all projects across all organizations
const allEnvironments = projectsByOrg.flatMap((project) => project.environments); const allEnvironments = projectsByOrg.flatMap((project) => project.environments);

View File

@@ -7,8 +7,8 @@ import { ITEMS_PER_PAGE } from "../constants";
import { import {
getProject, getProject,
getProjectByEnvironmentId, getProjectByEnvironmentId,
getProjectEnvironmentsByOrganizationIds,
getProjects, getProjects,
getUserProjectEnvironmentsByOrganizationIds,
getUserProjects, getUserProjects,
} from "./service"; } from "./service";
@@ -21,6 +21,7 @@ vi.mock("@formbricks/database", () => ({
}, },
membership: { membership: {
findFirst: vi.fn(), findFirst: vi.fn(),
findMany: vi.fn(),
}, },
}, },
})); }));
@@ -488,6 +489,7 @@ describe("Project Service", () => {
test("getProjectsByOrganizationIds should return projects for given organization IDs", async () => { test("getProjectsByOrganizationIds should return projects for given organization IDs", async () => {
const organizationId1 = createId(); const organizationId1 = createId();
const organizationId2 = createId(); const organizationId2 = createId();
const userId = createId();
const mockProjects = [ const mockProjects = [
{ {
environments: [], environments: [],
@@ -497,16 +499,34 @@ describe("Project Service", () => {
}, },
]; ];
vi.mocked(prisma.membership.findMany).mockResolvedValue([
{
userId,
organizationId: organizationId1,
role: "owner" as any,
accepted: true,
deprecatedRole: null,
},
{
userId,
organizationId: organizationId2,
role: "owner" as any,
accepted: true,
deprecatedRole: null,
},
]);
vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects as any); vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects as any);
const result = await getProjectEnvironmentsByOrganizationIds([organizationId1, organizationId2]); const result = await getUserProjectEnvironmentsByOrganizationIds(
[organizationId1, organizationId2],
userId
);
expect(result).toEqual(mockProjects); expect(result).toEqual(mockProjects);
expect(prisma.project.findMany).toHaveBeenCalledWith({ expect(prisma.project.findMany).toHaveBeenCalledWith({
where: { where: {
organizationId: { OR: [{ organizationId: organizationId1 }, { organizationId: organizationId2 }],
in: [organizationId1, organizationId2],
},
}, },
select: { environments: true }, select: { environments: true },
}); });
@@ -515,17 +535,36 @@ describe("Project Service", () => {
test("getProjectsByOrganizationIds should return empty array when no projects are found", async () => { test("getProjectsByOrganizationIds should return empty array when no projects are found", async () => {
const organizationId1 = createId(); const organizationId1 = createId();
const organizationId2 = createId(); const organizationId2 = createId();
const userId = createId();
vi.mocked(prisma.membership.findMany).mockResolvedValue([
{
userId,
organizationId: organizationId1,
role: "owner" as any,
accepted: true,
deprecatedRole: null,
},
{
userId,
organizationId: organizationId2,
role: "owner" as any,
accepted: true,
deprecatedRole: null,
},
]);
vi.mocked(prisma.project.findMany).mockResolvedValue([]); vi.mocked(prisma.project.findMany).mockResolvedValue([]);
const result = await getProjectEnvironmentsByOrganizationIds([organizationId1, organizationId2]); const result = await getUserProjectEnvironmentsByOrganizationIds(
[organizationId1, organizationId2],
userId
);
expect(result).toEqual([]); expect(result).toEqual([]);
expect(prisma.project.findMany).toHaveBeenCalledWith({ expect(prisma.project.findMany).toHaveBeenCalledWith({
where: { where: {
organizationId: { OR: [{ organizationId: organizationId1 }, { organizationId: organizationId2 }],
in: [organizationId1, organizationId2],
},
}, },
select: { environments: true }, select: { environments: true },
}); });
@@ -534,18 +573,111 @@ describe("Project Service", () => {
test("getProjectsByOrganizationIds should throw DatabaseError when prisma throws", async () => { test("getProjectsByOrganizationIds should throw DatabaseError when prisma throws", async () => {
const organizationId1 = createId(); const organizationId1 = createId();
const organizationId2 = createId(); const organizationId2 = createId();
const userId = createId();
const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", { const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
code: "P2002", code: "P2002",
clientVersion: "5.0.0", clientVersion: "5.0.0",
}); });
vi.mocked(prisma.membership.findMany).mockResolvedValue([
{
userId,
organizationId: organizationId1,
role: "owner" as any,
accepted: true,
deprecatedRole: null,
},
]);
vi.mocked(prisma.project.findMany).mockRejectedValue(prismaError); vi.mocked(prisma.project.findMany).mockRejectedValue(prismaError);
await expect(getProjectEnvironmentsByOrganizationIds([organizationId1, organizationId2])).rejects.toThrow( await expect(
DatabaseError getUserProjectEnvironmentsByOrganizationIds([organizationId1, organizationId2], userId)
); ).rejects.toThrow(DatabaseError);
}); });
test("getProjectsByOrganizationIds should throw ValidationError with wrong input", async () => { test("getProjectsByOrganizationIds should throw ValidationError with wrong input", async () => {
await expect(getProjectEnvironmentsByOrganizationIds(["wrong-id"])).rejects.toThrow(ValidationError); const userId = createId();
await expect(getUserProjectEnvironmentsByOrganizationIds(["wrong-id"], userId)).rejects.toThrow(
ValidationError
);
});
test("getProjectsByOrganizationIds should return empty array when user has no memberships", async () => {
const organizationId1 = createId();
const organizationId2 = createId();
const userId = createId();
// Mock no memberships found
vi.mocked(prisma.membership.findMany).mockResolvedValue([]);
const result = await getUserProjectEnvironmentsByOrganizationIds(
[organizationId1, organizationId2],
userId
);
expect(result).toEqual([]);
expect(prisma.membership.findMany).toHaveBeenCalledWith({
where: {
userId,
organizationId: {
in: [organizationId1, organizationId2],
},
},
});
// Should not call project.findMany when no memberships
expect(prisma.project.findMany).not.toHaveBeenCalled();
});
test("getProjectsByOrganizationIds should handle member role with team access", async () => {
const organizationId1 = createId();
const organizationId2 = createId();
const userId = createId();
const mockProjects = [
{
environments: [],
},
];
// Mock membership where user is a member
vi.mocked(prisma.membership.findMany).mockResolvedValue([
{
userId,
organizationId: organizationId1,
role: "member" as any,
accepted: true,
deprecatedRole: null,
},
]);
vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects as any);
const result = await getUserProjectEnvironmentsByOrganizationIds(
[organizationId1, organizationId2],
userId
);
expect(result).toEqual(mockProjects);
expect(prisma.project.findMany).toHaveBeenCalledWith({
where: {
OR: [
{
organizationId: organizationId1,
projectTeams: {
some: {
team: {
teamUsers: {
some: {
userId,
},
},
},
},
},
},
],
},
select: { environments: true },
});
}); });
}); });

View File

@@ -171,20 +171,56 @@ export const getOrganizationProjectsCount = reactCache(async (organizationId: st
} }
}); });
export const getProjectEnvironmentsByOrganizationIds = reactCache( export const getUserProjectEnvironmentsByOrganizationIds = reactCache(
async (organizationIds: string[]): Promise<Pick<TProject, "environments">[]> => { async (organizationIds: string[], userId: string): Promise<Pick<TProject, "environments">[]> => {
validateInputs([organizationIds, ZId.array()]); validateInputs([organizationIds, ZId.array()], [userId, ZId]);
try { try {
if (organizationIds.length === 0) { if (organizationIds.length === 0) {
return []; return [];
} }
const projects = await prisma.project.findMany({ const memberships = await prisma.membership.findMany({
where: { where: {
userId,
organizationId: { organizationId: {
in: organizationIds, in: organizationIds,
}, },
}, },
});
if (memberships.length === 0) {
return [];
}
const whereConditions: Prisma.ProjectWhereInput[] = memberships.map((membership) => {
let projectWhereClause: Prisma.ProjectWhereInput = {
organizationId: membership.organizationId,
};
if (membership.role === "member") {
projectWhereClause = {
...projectWhereClause,
projectTeams: {
some: {
team: {
teamUsers: {
some: {
userId,
},
},
},
},
},
};
}
return projectWhereClause;
});
const projects = await prisma.project.findMany({
where: {
OR: whereConditions,
},
select: { environments: true }, select: { environments: true },
}); });