refactor: use userRepository class methods for signup handlers (#29561)
* refactor: use userRepository class methods for signup handlers * feat: add user repo modules for DI * feat: build user repo container * test: integration test for user repository * fix: use DI container in selfhosted * refactor: tests, module, container and handlers --------- Co-authored-by: Bandhan Majumder <bandhanmajumder16@gmail.com>
This commit is contained in:
co-authored by
Bandhan Majumder
parent
642c38582e
commit
dcd0da831f
@@ -0,0 +1,10 @@
|
||||
import { createContainer } from "@calcom/features/di/di";
|
||||
import { moduleLoader as userRepositoryModuleLoader } from "@calcom/features/di/modules/User";
|
||||
import type { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
|
||||
const userRepositoryContainer = createContainer();
|
||||
|
||||
export function getUserRepository(): UserRepository {
|
||||
userRepositoryModuleLoader.loadModule(userRepositoryContainer);
|
||||
return userRepositoryContainer.get<UserRepository>(userRepositoryModuleLoader.token);
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import { UserRepository } from "./UserRepository";
|
||||
import { getUserRepository } from "@calcom/features/di/containers/UserRepository";
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
const testRunId = `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
||||
const createdUserIds: number[] = [];
|
||||
|
||||
describe("UserRepository Integration Tests - Signup Methods", () => {
|
||||
let userRepository: UserRepository;
|
||||
|
||||
async function cleanupTestUsers() {
|
||||
if (createdUserIds.length > 0) {
|
||||
await userRepository.deleteMany({ userIds: createdUserIds })
|
||||
createdUserIds.length = 0
|
||||
}
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
userRepository = getUserRepository();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanupTestUsers();
|
||||
});
|
||||
|
||||
describe("upsertForSignup", () => {
|
||||
it("should create a new user when email doesn't exist", async () => {
|
||||
const testEmail = `signup-new-${testRunId}@example.com`;
|
||||
const hashedPassword = await bcrypt.hash("password123", 10);
|
||||
|
||||
const result = await userRepository.upsertForSignup({
|
||||
email: testEmail,
|
||||
username: `signupuser-${testRunId}`,
|
||||
hashedPassword,
|
||||
organizationId: null,
|
||||
emailVerified: new Date(Date.now()),
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.id).toBeDefined();
|
||||
createdUserIds.push(result.id);
|
||||
|
||||
const createdUser = await userRepository.findByEmail({ email: testEmail});
|
||||
|
||||
expect(createdUser).not.toBeNull();
|
||||
expect(createdUser?.email).toBe(testEmail);
|
||||
expect(createdUser?.emailVerified).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should update existing user's credentials on upsert", async () => {
|
||||
const testEmail = `signup-update-${testRunId}@example.com`;
|
||||
const hashedPassword1 = await bcrypt.hash("password123", 10);
|
||||
const hashedPassword2 = await bcrypt.hash("newpassword456", 10);
|
||||
|
||||
const user1 = await userRepository.upsertForSignup({
|
||||
email: testEmail,
|
||||
username: `user-${testRunId}`,
|
||||
hashedPassword: hashedPassword1,
|
||||
organizationId: null,
|
||||
emailVerified: new Date(Date.now() - 86400000),
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
});
|
||||
createdUserIds.push(user1.id);
|
||||
|
||||
const user2 = await userRepository.upsertForSignup({
|
||||
email: testEmail,
|
||||
username: `user-${testRunId}-updated`,
|
||||
hashedPassword: hashedPassword2,
|
||||
organizationId: null,
|
||||
emailVerified: new Date(),
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
});
|
||||
|
||||
expect(user2.id).toBe(user1.id);
|
||||
|
||||
const updatedUser = await userRepository.findByEmailAndIncludeProfilesAndPassword({ email: testEmail});
|
||||
|
||||
expect(updatedUser?.username).toBe(`user-${testRunId}-updated`);
|
||||
expect(updatedUser?.password?.hash).toBe(hashedPassword2);
|
||||
});
|
||||
|
||||
it("should preserve user ID on upsert", async () => {
|
||||
const testEmail = `signup-preserve-${testRunId}@example.com`;
|
||||
const hashedPassword = await bcrypt.hash("password123", 10);
|
||||
|
||||
const user1 = await userRepository.upsertForSignup({
|
||||
email: testEmail,
|
||||
username: `user-${testRunId}-p1`,
|
||||
hashedPassword,
|
||||
organizationId: null,
|
||||
emailVerified: new Date(),
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
});
|
||||
createdUserIds.push(user1.id);
|
||||
const initialId = user1.id;
|
||||
|
||||
const user2 = await userRepository.upsertForSignup({
|
||||
email: testEmail,
|
||||
username: `user-${testRunId}-p2`,
|
||||
hashedPassword,
|
||||
organizationId: null,
|
||||
emailVerified: new Date(),
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
});
|
||||
|
||||
expect(user2.id).toBe(initialId);
|
||||
});
|
||||
|
||||
it("should set email as verified on signup", async () => {
|
||||
const testEmail = `signup-verified-${testRunId}@example.com`;
|
||||
const hashedPassword = await bcrypt.hash("password123", 10);
|
||||
const verificationDate = new Date();
|
||||
|
||||
const result = await userRepository.upsertForSignup({
|
||||
email: testEmail,
|
||||
username: `user-${testRunId}-v1`,
|
||||
hashedPassword,
|
||||
organizationId: null,
|
||||
emailVerified: verificationDate,
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
});
|
||||
createdUserIds.push(result.id);
|
||||
|
||||
const user = await userRepository.findByEmail({ email: testEmail });
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User was not found after signup");
|
||||
}
|
||||
|
||||
expect(user?.emailVerified).not.toBeNull();
|
||||
expect(user?.emailVerified?.getTime()).toBe(verificationDate.getTime());
|
||||
});
|
||||
|
||||
it("should create password record for new user", async () => {
|
||||
const testEmail = `signup-pwd-${testRunId}@example.com`;
|
||||
const hashedPassword = await bcrypt.hash("password123", 10);
|
||||
|
||||
const result = await userRepository.upsertForSignup({
|
||||
email: testEmail,
|
||||
username: `user-${testRunId}-pwd`,
|
||||
hashedPassword,
|
||||
organizationId: null,
|
||||
emailVerified: new Date(),
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
});
|
||||
createdUserIds.push(result.id);
|
||||
|
||||
const userWithPassword = await userRepository.findByEmailAndIncludeProfilesAndPassword({ email: testEmail })
|
||||
|
||||
if (!userWithPassword) {
|
||||
throw new Error("User was not found after signup");
|
||||
}
|
||||
|
||||
expect(userWithPassword?.password).not.toBeNull();
|
||||
expect(userWithPassword?.password?.hash).toBe(hashedPassword);
|
||||
expect(userWithPassword?.password?.userId).toBe(result.id);
|
||||
});
|
||||
|
||||
it("should throw error on duplicate unique constraint if username exists", async () => {
|
||||
const testEmail1 = `signup-dup1-${testRunId}@example.com`;
|
||||
const testEmail2 = `signup-dup2-${testRunId}@example.com`;
|
||||
const duplicateUsername = `dupuser-${testRunId}`;
|
||||
const hashedPassword = await bcrypt.hash("password123", 10);
|
||||
|
||||
const user1 = await userRepository.upsertForSignup({
|
||||
email: testEmail1,
|
||||
username: duplicateUsername,
|
||||
hashedPassword,
|
||||
organizationId: null,
|
||||
emailVerified: new Date(),
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
});
|
||||
createdUserIds.push(user1.id);
|
||||
|
||||
await expect(
|
||||
userRepository.upsertForSignup({
|
||||
email: testEmail2,
|
||||
username: duplicateUsername,
|
||||
hashedPassword,
|
||||
organizationId: null,
|
||||
emailVerified: new Date(),
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Integration: Individual Signup flow", () => {
|
||||
it("should complete full individual signup flow: check email, check username, then upsert", async () => {
|
||||
const testEmail = `flow-${testRunId}@example.com`;
|
||||
const testUsername = `flowuser-${testRunId}`;
|
||||
const hashedPassword = await bcrypt.hash("password123", 10);
|
||||
|
||||
const existingUser = await userRepository.findByEmail({
|
||||
email: testEmail,
|
||||
});
|
||||
expect(existingUser).toBeNull();
|
||||
|
||||
const usersWithUsername = await userRepository.findUsersByUsername({
|
||||
orgSlug: null,
|
||||
usernameList: [testUsername],
|
||||
});
|
||||
const usernameTaken = usersWithUsername.length > 0 ? usersWithUsername[0] : null;
|
||||
expect(usernameTaken).toBeNull();
|
||||
|
||||
const signupResult = await userRepository.upsertForSignup({
|
||||
email: testEmail,
|
||||
username: testUsername,
|
||||
hashedPassword,
|
||||
organizationId: null,
|
||||
emailVerified: new Date(Date.now()),
|
||||
identityProvider: IdentityProvider.CAL,
|
||||
});
|
||||
createdUserIds.push(signupResult.id);
|
||||
|
||||
expect(signupResult.id).toBeDefined();
|
||||
|
||||
const userWithPassword = await userRepository.findByEmailAndIncludeProfilesAndPassword({ email: testEmail })
|
||||
|
||||
if (!userWithPassword) {
|
||||
throw new Error("User was not found after signup");
|
||||
}
|
||||
|
||||
expect(userWithPassword?.email).toBe(testEmail);
|
||||
expect(userWithPassword?.username).toBe(testUsername);
|
||||
expect(userWithPassword?.password).not.toBeNull();
|
||||
|
||||
const userWithOrganizationId = await userRepository.getUserOrganizationAndTeams({ userId: userWithPassword.id});
|
||||
|
||||
expect(userWithOrganizationId?.organizationId).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import type { PrismaClient } from "@calcom/prisma";
|
||||
import { availabilityUserSelect } from "@calcom/prisma";
|
||||
import type { DestinationCalendar, SelectedCalendar, User as UserType } from "@calcom/prisma/client";
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
import type { IdentityProvider } from "@calcom/prisma/enums";
|
||||
import type { CreationSource } from "@calcom/prisma/enums";
|
||||
import { BookingStatus, MembershipRole } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
@@ -896,20 +897,19 @@ export class UserRepository {
|
||||
|
||||
async create(
|
||||
data: Omit<Prisma.UserCreateInput, "password" | "organization" | "movedToProfile"> & {
|
||||
username: string;
|
||||
username: string | null;
|
||||
hashedPassword?: string;
|
||||
organizationId: number | null;
|
||||
creationSource: CreationSource;
|
||||
locked: boolean;
|
||||
}
|
||||
) {
|
||||
const organizationIdValue = data.organizationId;
|
||||
const { email, username, creationSource, locked, hashedPassword, ...rest } = data;
|
||||
const { email, username, creationSource, locked, hashedPassword, organizationId, ...rest } = data;
|
||||
|
||||
logger.info("create user", {
|
||||
log.info("create user", {
|
||||
email,
|
||||
username,
|
||||
organizationIdValue,
|
||||
organizationId,
|
||||
locked,
|
||||
});
|
||||
const t = await getTranslation("en", "common");
|
||||
@@ -939,13 +939,13 @@ export class UserRepository {
|
||||
},
|
||||
creationSource,
|
||||
locked,
|
||||
...(organizationIdValue
|
||||
...(organizationId && username
|
||||
? {
|
||||
organizationId: organizationIdValue,
|
||||
organizationId,
|
||||
profiles: {
|
||||
create: {
|
||||
username,
|
||||
organizationId: organizationIdValue,
|
||||
organizationId,
|
||||
uid: ProfileRepository.generateProfileUid(),
|
||||
},
|
||||
},
|
||||
@@ -1419,6 +1419,14 @@ export class UserRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteMany({ userIds }: {userIds: number[]}){
|
||||
await this.prismaClient.user.deleteMany({
|
||||
where: {
|
||||
id: { in: userIds },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a user by ID returning only their username
|
||||
* @param userId - The user ID
|
||||
@@ -1480,6 +1488,38 @@ export class UserRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findByEmailWithInvitedTo({ email }: { email: string } ) {
|
||||
return this.prismaClient.user.findUnique({
|
||||
where: {
|
||||
email: email.toLowerCase()
|
||||
},
|
||||
select: {
|
||||
invitedTo: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async findByUsernameAndOrganizationId({
|
||||
username,
|
||||
organizationId,
|
||||
excludeEmail,
|
||||
}: {
|
||||
username: string,
|
||||
organizationId: number | null,
|
||||
excludeEmail: string
|
||||
}) {
|
||||
return this.prismaClient.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
organizationId,
|
||||
NOT: { email: excludeEmail }
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async lockByEmail({ email }: { email: string }) {
|
||||
await this.prismaClient.user.updateMany({
|
||||
where: { email },
|
||||
@@ -1506,4 +1546,45 @@ export class UserRepository {
|
||||
|
||||
return { email: user.email, username: user.username };
|
||||
}
|
||||
|
||||
async upsertForSignup({
|
||||
email,
|
||||
username,
|
||||
hashedPassword,
|
||||
organizationId,
|
||||
emailVerified,
|
||||
identityProvider,
|
||||
}: {
|
||||
email: string;
|
||||
username: string | null;
|
||||
hashedPassword: string;
|
||||
organizationId: number | null;
|
||||
emailVerified: Date;
|
||||
identityProvider: IdentityProvider;
|
||||
}) {
|
||||
return this.prismaClient.user.upsert({
|
||||
where: { email },
|
||||
update: {
|
||||
username,
|
||||
emailVerified,
|
||||
identityProvider,
|
||||
password: {
|
||||
upsert: {
|
||||
create: { hash: hashedPassword },
|
||||
update: { hash: hashedPassword },
|
||||
},
|
||||
},
|
||||
organizationId,
|
||||
},
|
||||
create: {
|
||||
username,
|
||||
email,
|
||||
emailVerified,
|
||||
identityProvider,
|
||||
password: { create: { hash: hashedPassword } },
|
||||
organizationId,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user