Skip to content

Commit 47dccdf

Browse files
committed
Change member email when identity email is set
1 parent 05158e5 commit 47dccdf

File tree

2 files changed

+83
-13
lines changed

2 files changed

+83
-13
lines changed

src/api/user/user.service.spec.ts

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,13 @@ const mockEventService: jest.Mocked<Partial<EventService>> = {
138138
postDirectBusMessage: jest.fn(),
139139
};
140140

141-
const mockMemberPrisma: Partial<MemberPrismaService> = {
141+
const memberUpdateMock = jest.fn();
142+
const mockMemberPrisma: any = {
142143
// Only the parts used by UserService need to be mocked
143144
member: {
144145
create: jest.fn(),
145-
} as any,
146+
update: memberUpdateMock,
147+
},
146148
};
147149

148150
const mockConfigService = {
@@ -1562,6 +1564,8 @@ describe('UserService', () => {
15621564
beforeEach(() => {
15631565
jest.clearAllMocks();
15641566

1567+
memberUpdateMock.mockResolvedValue(undefined);
1568+
15651569
// Mock checkEmailAvailabilityForUser
15661570
mockCheckEmail = jest
15671571
.spyOn(service, 'checkEmailAvailabilityForUser')
@@ -1652,6 +1656,10 @@ describe('UserService', () => {
16521656
{ userId: 1, handle: 'testuser' },
16531657
);
16541658
expect(result).toEqual(mockUser);
1659+
expect(memberUpdateMock).toHaveBeenCalledWith({
1660+
where: { userId },
1661+
data: { email: newEmail.toLowerCase() },
1662+
});
16551663
});
16561664

16571665
it('should throw BadRequestException for invalid user ID format', async () => {
@@ -1863,6 +1871,48 @@ describe('UserService', () => {
18631871
);
18641872
});
18651873

1874+
it('should log an error if members.member update fails but continue', async () => {
1875+
const txMock = {
1876+
user: {
1877+
findUnique: jest.fn().mockResolvedValue(mockUser),
1878+
update: jest.fn().mockResolvedValue(mockUser),
1879+
},
1880+
email: {
1881+
findFirst: jest
1882+
.fn()
1883+
.mockResolvedValueOnce(mockCurrentEmailRecord)
1884+
.mockResolvedValueOnce(null),
1885+
update: jest.fn().mockResolvedValue(mockUpdatedEmailRecord),
1886+
},
1887+
};
1888+
mockPrismaOltp.$transaction.mockImplementation(
1889+
<T>(callback): Promise<T> => {
1890+
const result = callback(txMock);
1891+
return result instanceof Promise ? result : Promise.resolve(result);
1892+
},
1893+
);
1894+
1895+
memberUpdateMock.mockRejectedValueOnce(
1896+
new Error('Member update failed'),
1897+
);
1898+
1899+
const result = await service.updatePrimaryEmail(
1900+
userIdString,
1901+
newEmail,
1902+
mockAuthUser,
1903+
);
1904+
1905+
expect(result).toEqual(mockUser);
1906+
expect(memberUpdateMock).toHaveBeenCalledWith({
1907+
where: { userId },
1908+
data: { email: newEmail.toLowerCase() },
1909+
});
1910+
expect(loggerErrorSpy).toHaveBeenCalledWith(
1911+
expect.stringContaining('Failed to update members.member email'),
1912+
expect.any(String),
1913+
);
1914+
});
1915+
18661916
it('should handle case when updated email record is not found after transaction', async () => {
18671917
// Set up transaction mock
18681918
const txMock = {
@@ -1965,6 +2015,7 @@ describe('UserService', () => {
19652015
const userIdString = '1';
19662016
const oldStatus = 'U';
19672017
const newStatus = 'A'; // Unverified to Active
2018+
const statusComment = 'Unit test comment';
19682019
const mockExistingUser = createMockUserModel({
19692020
user_id: new Decimal(userId),
19702021
status: oldStatus,
@@ -1992,6 +2043,7 @@ describe('UserService', () => {
19922043
userIdString,
19932044
newStatus,
19942045
mockUser,
2046+
statusComment,
19952047
);
19962048

19972049
expect(prismaOltp.user.update).toHaveBeenCalledWith({
@@ -2023,7 +2075,7 @@ describe('UserService', () => {
20232075
prismaOltp.user.findUnique.mockResolvedValue(fromActiveUser);
20242076
prismaOltp.user.update.mockResolvedValue(toInactiveUser);
20252077

2026-
await service.updateStatus(userIdString, 'I', mockUser);
2078+
await service.updateStatus(userIdString, 'I', mockUser, statusComment);
20272079
expect(mockEventService.postEnvelopedNotification).toHaveBeenCalledWith(
20282080
'event.user.deactivated',
20292081
service.toCamelCase(toInactiveUser),
@@ -2037,17 +2089,17 @@ describe('UserService', () => {
20372089

20382090
it('should throw BadRequest for invalid user ID or status code', async () => {
20392091
await expect(
2040-
service.updateStatus('abc', newStatus, mockUser),
2092+
service.updateStatus('abc', newStatus, mockUser, statusComment),
20412093
).rejects.toThrow(BadRequestException);
20422094
await expect(
2043-
service.updateStatus(userIdString, 'X', mockUser),
2095+
service.updateStatus(userIdString, 'X', mockUser, statusComment),
20442096
).rejects.toThrow(BadRequestException);
20452097
});
20462098

20472099
it('should throw NotFoundException if user not found', async () => {
20482100
prismaOltp.user.findUnique.mockResolvedValue(null);
20492101
await expect(
2050-
service.updateStatus(userIdString, newStatus, mockUser),
2102+
service.updateStatus(userIdString, newStatus, mockUser, statusComment),
20512103
).rejects.toThrow(NotFoundException);
20522104
});
20532105

@@ -2056,6 +2108,7 @@ describe('UserService', () => {
20562108
userIdString,
20572109
oldStatus,
20582110
mockUser,
2111+
statusComment,
20592112
);
20602113
expect(prismaOltp.user.update).not.toHaveBeenCalled();
20612114
expect(result).toEqual(mockExistingUser);

src/api/user/user.service.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1451,6 +1451,8 @@ export class UserService {
14511451
if (isNaN(userId)) {
14521452
throw new BadRequestException('Invalid user ID format.');
14531453
}
1454+
const normalizedEmail = newEmail.toLowerCase();
1455+
let emailChanged = false;
14541456

14551457
this.logger.log(
14561458
`Attempting to update primary email for user ID: ${userId} to ${newEmail} by admin ${authUser.userId}`,
@@ -1512,10 +1514,11 @@ export class UserService {
15121514
await tx.email.update({
15131515
where: { email_id: currentPrimaryEmailRecord.email_id },
15141516
data: {
1515-
address: newEmail.toLowerCase(),
1517+
address: normalizedEmail,
15161518
modify_date: new Date(),
15171519
},
15181520
});
1521+
emailChanged = true;
15191522

15201523
this.logger.log(
15211524
`Updated existing primary email record ${currentPrimaryEmailRecord.email_id.toNumber()} from ${oldEmail} to ${newEmail} for user ${userId}`,
@@ -1554,6 +1557,23 @@ export class UserService {
15541557
);
15551558
}
15561559

1560+
if (emailChanged) {
1561+
try {
1562+
await this.memberPrisma.member.update({
1563+
where: { userId },
1564+
data: { email: normalizedEmail },
1565+
});
1566+
this.logger.log(
1567+
`Updated members.member email to ${normalizedEmail} for user ${userId}`,
1568+
);
1569+
} catch (error) {
1570+
this.logger.error(
1571+
`Failed to update members.member email for user ${userId}: ${error.message}`,
1572+
error.stack,
1573+
);
1574+
}
1575+
}
1576+
15571577
return updatedUserInTx;
15581578
}
15591579

@@ -2172,8 +2192,7 @@ export class UserService {
21722192

21732193
// Send Welcome Email directly, matching legacy Java behavior
21742194
if (emailAddress && user?.handle) {
2175-
const domain =
2176-
this.configService.get<string>('APP_DOMAIN') || 'topcoder-dev.com';
2195+
const domain = CommonUtils.getAppDomain(this.configService);
21772196
const fromEmail = `Topcoder <noreply@${domain}>`;
21782197
let welcomeTemplateId = this.configService.get<string>(
21792198
'SENDGRID_WELCOME_EMAIL_TEMPLATE_ID',
@@ -2289,8 +2308,7 @@ export class UserService {
22892308
email: string,
22902309
regSource?: string,
22912310
) {
2292-
const domain =
2293-
this.configService.get<string>('APP_DOMAIN') || 'topcoder-dev.com';
2311+
const domain = CommonUtils.getAppDomain(this.configService);
22942312
const fromEmail = `Topcoder <noreply@${domain}>`;
22952313
const sendGridTemplateId = this.configService.get<string>(
22962314
'SENDGRID_RESEND_ACTIVATION_EMAIL_TEMPLATE_ID',
@@ -2349,8 +2367,7 @@ export class UserService {
23492367
async resendActivationEmailEvent(userOtp: UserOtpDto, primaryEmail: string) {
23502368
try {
23512369
// For activation email (resend), use postDirectBusMessage to match legacy Java structure
2352-
const domain =
2353-
this.configService.get<string>('APP_DOMAIN') || 'topcoder-dev.com';
2370+
const domain = CommonUtils.getAppDomain(this.configService);
23542371
const fromEmail = `Topcoder <noreply@${domain}>`;
23552372
// Use the specific template ID for resending activation emails
23562373
const sendgridTemplateId = this.configService.get<string>(

0 commit comments

Comments
 (0)