mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
refactor(iam): remove remaining prisma.user refs from passenger side align RegisterDto to IAM body
This commit is contained in:
@@ -15,12 +15,6 @@ function buildPrismaMock() {
|
||||
bookingSeat: {
|
||||
updateMany: jest.fn(),
|
||||
},
|
||||
user: {
|
||||
findUnique: jest.fn(),
|
||||
findFirst: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
passenger: { create: jest.fn() },
|
||||
loyaltyAccount: { create: jest.fn() },
|
||||
walletAccount: { create: jest.fn() },
|
||||
@@ -29,6 +23,10 @@ function buildPrismaMock() {
|
||||
};
|
||||
}
|
||||
|
||||
function buildDataSourceMock() {
|
||||
return { query: jest.fn().mockResolvedValue([]) };
|
||||
}
|
||||
|
||||
function buildConfig(overrides?: Partial<FaydaConfig>): FaydaConfig {
|
||||
return {
|
||||
enabled: true,
|
||||
@@ -58,6 +56,7 @@ function buildConfigService(faydaConfig: FaydaConfig): jest.Mocked<ConfigService
|
||||
|
||||
describe('VerifaydaService (OIDC, client-callback)', () => {
|
||||
let prisma: ReturnType<typeof buildPrismaMock>;
|
||||
let dataSource: ReturnType<typeof buildDataSourceMock>;
|
||||
let service: VerifaydaService;
|
||||
let realPrivateJwk: JWK;
|
||||
|
||||
@@ -69,11 +68,12 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
prisma = buildPrismaMock();
|
||||
dataSource = buildDataSourceMock();
|
||||
const cfg = buildConfig({ privateJwk: realPrivateJwk as FaydaConfig['privateJwk'] });
|
||||
service = new VerifaydaService(
|
||||
buildConfigService(cfg),
|
||||
prisma as unknown as PrismaService,
|
||||
{ query: jest.fn().mockResolvedValue([]) } as any,
|
||||
dataSource as any,
|
||||
);
|
||||
(global as any).fetch = jest.fn();
|
||||
});
|
||||
@@ -127,7 +127,7 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
||||
const disabledService = new VerifaydaService(
|
||||
buildConfigService(buildConfig({ enabled: false })),
|
||||
prisma as unknown as PrismaService,
|
||||
{ query: jest.fn().mockResolvedValue([]) } as any,
|
||||
buildDataSourceMock() as any,
|
||||
);
|
||||
await expect(
|
||||
disabledService.startVerification({ purpose: 'PURCHASE' }),
|
||||
@@ -147,7 +147,7 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
||||
status: 'PENDING',
|
||||
errorCode: null,
|
||||
errorDescription: null,
|
||||
userId: null,
|
||||
iamUserId: null,
|
||||
bookingId: null,
|
||||
expiresAt: new Date(Date.now() + 60_000),
|
||||
...overrides,
|
||||
@@ -210,7 +210,7 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
||||
platform: 'WEB',
|
||||
saveToAccount: false,
|
||||
status: 'PENDING',
|
||||
userId: null,
|
||||
iamUserId: null,
|
||||
bookingId: null,
|
||||
expiresAt: new Date(Date.now() + 60_000),
|
||||
...overrides,
|
||||
@@ -262,12 +262,15 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('saves to the User account when saveToAccount=true and no conflict', async () => {
|
||||
it('saves to the IAM user account when saveToAccount=true and no conflict', async () => {
|
||||
prisma.faydaVerificationSession.findUnique.mockResolvedValue(
|
||||
pendingSession({ userId: 'user-1', saveToAccount: true }),
|
||||
pendingSession({ iamUserId: 'iam-user-1', saveToAccount: true }),
|
||||
);
|
||||
prisma.user.findFirst.mockResolvedValue(null);
|
||||
prisma.user.update.mockResolvedValue({});
|
||||
// First dataSource.query = conflict check returns [] (no conflict)
|
||||
// Second dataSource.query = UPDATE call returns []
|
||||
dataSource.query
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([]);
|
||||
prisma.faydaVerificationSession.update.mockResolvedValue({});
|
||||
|
||||
mockFetchSequence(
|
||||
@@ -285,17 +288,24 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
||||
});
|
||||
|
||||
expect(result.verified).toBe(true);
|
||||
expect(prisma.user.update).toHaveBeenCalledWith({
|
||||
where: { id: 'user-1' },
|
||||
data: expect.objectContaining({ faydaVerified: true, faydaSub: 'fayda-sub-2' }),
|
||||
});
|
||||
// conflict check: SELECT id FROM iam.users WHERE metadata->>'faydaSub' = ...
|
||||
expect(dataSource.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`metadata->>'faydaSub'`),
|
||||
['fayda-sub-2', 'iam-user-1'],
|
||||
);
|
||||
// update: SET metadata = COALESCE(metadata, '{}') || ...
|
||||
expect(dataSource.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('UPDATE iam.users SET metadata'),
|
||||
expect.arrayContaining(['iam-user-1']),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws identity_conflict (409) when faydaSub belongs to another user', async () => {
|
||||
it('throws identity_conflict (409) when faydaSub belongs to another IAM user', async () => {
|
||||
prisma.faydaVerificationSession.findUnique.mockResolvedValue(
|
||||
pendingSession({ userId: 'user-1', saveToAccount: true }),
|
||||
pendingSession({ iamUserId: 'iam-user-1', saveToAccount: true }),
|
||||
);
|
||||
prisma.user.findFirst.mockResolvedValue({ id: 'other-user' });
|
||||
// conflict check returns a conflicting row
|
||||
dataSource.query.mockResolvedValueOnce([{ id: 'other-iam-user' }]);
|
||||
prisma.faydaVerificationSession.updateMany.mockResolvedValue({ count: 1 });
|
||||
|
||||
mockFetchSequence(
|
||||
@@ -310,7 +320,8 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
||||
await expect(
|
||||
service.completeVerification({ code: 'authcode', state: 'state-abc' }),
|
||||
).rejects.toMatchObject({ status: 409 });
|
||||
expect(prisma.user.update).not.toHaveBeenCalled();
|
||||
// UPDATE must not have been called
|
||||
expect(dataSource.query).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('throws 502 when the token endpoint returns 4xx', async () => {
|
||||
@@ -384,7 +395,7 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
||||
platform: 'WEB',
|
||||
saveToAccount: false,
|
||||
status: 'PENDING',
|
||||
userId: null,
|
||||
iamUserId: null,
|
||||
bookingId: null,
|
||||
expiresAt: new Date(Date.now() + 60_000),
|
||||
...overrides,
|
||||
@@ -411,131 +422,41 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
||||
(global as any).fetch = jest.fn(() => Promise.resolve(queue.shift()));
|
||||
}
|
||||
|
||||
/** user.findUnique answers the faydaSub lookup and the issueLoginToken id lookup. */
|
||||
function mockUserFindUnique(bySub: any, fullUser: any) {
|
||||
prisma.user.findUnique.mockImplementation(async (args: any) => {
|
||||
if (args?.where?.faydaSub !== undefined) return bySub;
|
||||
if (args?.where?.id !== undefined) return fullUser;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
prisma.faydaVerificationSession.findUnique.mockResolvedValue(loginSession());
|
||||
});
|
||||
|
||||
it('creates a new user but rejects legacy local token issuance', async () => {
|
||||
const fullUser = {
|
||||
id: 'new-user',
|
||||
email: 'new@example.com',
|
||||
role: 'PASSENGER',
|
||||
passenger: { id: 'p-new' },
|
||||
agent: null,
|
||||
};
|
||||
mockUserFindUnique(null, fullUser);
|
||||
prisma.user.findFirst.mockResolvedValue(null);
|
||||
prisma.user.create.mockResolvedValue({ id: 'new-user' });
|
||||
prisma.passenger.create.mockResolvedValue({ id: 'p-new' });
|
||||
prisma.loyaltyAccount.create.mockResolvedValue({});
|
||||
prisma.walletAccount.create.mockResolvedValue({});
|
||||
prisma.userPreferences.create.mockResolvedValue({});
|
||||
prisma.faydaVerificationSession.update.mockResolvedValue({});
|
||||
|
||||
it('always rejects with FAYDA_LOGIN_MIGRATED_TO_IAM (401)', async () => {
|
||||
mockLoginFetch({ sub: 'login-sub-1', name: 'New Person', email: 'new@example.com' });
|
||||
|
||||
await expect(
|
||||
service.completeVerification({
|
||||
code: 'c',
|
||||
state: 'state-login',
|
||||
}),
|
||||
).rejects.toMatchObject({ status: 401 });
|
||||
expect(prisma.user.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
faydaSub: 'login-sub-1',
|
||||
faydaVerified: true,
|
||||
email: 'new@example.com',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(prisma.passenger.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('resolves an existing linked user but rejects legacy local token issuance', async () => {
|
||||
const fullUser = {
|
||||
id: 'known-user',
|
||||
email: 'k@example.com',
|
||||
role: 'PASSENGER',
|
||||
passenger: { id: 'p-k' },
|
||||
agent: null,
|
||||
};
|
||||
mockUserFindUnique({ id: 'known-user' }, fullUser);
|
||||
prisma.faydaVerificationSession.update.mockResolvedValue({});
|
||||
|
||||
mockLoginFetch({ sub: 'login-sub-2', name: 'Known' });
|
||||
|
||||
await expect(
|
||||
service.completeVerification({
|
||||
code: 'c',
|
||||
state: 'state-login',
|
||||
}),
|
||||
).rejects.toMatchObject({ status: 401 });
|
||||
expect(prisma.user.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('links Fayda to an existing account matched by email but rejects legacy local token issuance', async () => {
|
||||
const fullUser = {
|
||||
id: 'acc-1',
|
||||
email: 'match@example.com',
|
||||
role: 'PASSENGER',
|
||||
passenger: { id: 'p-1' },
|
||||
agent: null,
|
||||
};
|
||||
mockUserFindUnique(null, fullUser);
|
||||
prisma.user.findFirst.mockResolvedValue({ id: 'acc-1', faydaSub: null });
|
||||
prisma.user.update.mockResolvedValue({});
|
||||
prisma.faydaVerificationSession.update.mockResolvedValue({});
|
||||
|
||||
mockLoginFetch({ sub: 'login-sub-3', email: 'match@example.com' });
|
||||
|
||||
await expect(
|
||||
service.completeVerification({
|
||||
code: 'c',
|
||||
state: 'state-login',
|
||||
}),
|
||||
).rejects.toMatchObject({ status: 401 });
|
||||
expect(prisma.user.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: 'acc-1' },
|
||||
data: expect.objectContaining({ faydaSub: 'login-sub-3' }),
|
||||
}),
|
||||
);
|
||||
expect(prisma.user.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws identity_conflict (409) when matched account has a different faydaSub', async () => {
|
||||
mockUserFindUnique(null, null);
|
||||
prisma.user.findFirst.mockResolvedValue({ id: 'acc-2', faydaSub: 'someone-else' });
|
||||
prisma.faydaVerificationSession.updateMany.mockResolvedValue({ count: 1 });
|
||||
|
||||
mockLoginFetch({ sub: 'login-sub-4', email: 'match@example.com' });
|
||||
|
||||
await expect(
|
||||
service.completeVerification({ code: 'c', state: 'state-login' }),
|
||||
).rejects.toMatchObject({ status: 409 });
|
||||
expect(prisma.user.update).not.toHaveBeenCalled();
|
||||
expect(prisma.user.create).not.toHaveBeenCalled();
|
||||
).rejects.toMatchObject({
|
||||
status: 401,
|
||||
response: expect.objectContaining({ code: 'FAYDA_LOGIN_MIGRATED_TO_IAM' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not touch the database for LOGIN purpose', async () => {
|
||||
mockLoginFetch({ sub: 'login-sub-2', name: 'Person' });
|
||||
prisma.faydaVerificationSession.updateMany.mockResolvedValue({ count: 1 });
|
||||
|
||||
await expect(
|
||||
service.completeVerification({ code: 'c', state: 'state-login' }),
|
||||
).rejects.toMatchObject({ status: 401 });
|
||||
expect(dataSource.query).not.toHaveBeenCalled();
|
||||
expect(prisma.passenger.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVerificationStatus', () => {
|
||||
it('returns verified=true when User row has the flag', async () => {
|
||||
prisma.user.findUnique.mockResolvedValue({
|
||||
faydaVerified: true,
|
||||
faydaVerifiedAt: new Date('2026-01-01T00:00:00Z'),
|
||||
fullName: 'Test User',
|
||||
});
|
||||
const result = await service.getVerificationStatus('user-1');
|
||||
it('returns verified=true when IAM user metadata has the flag', async () => {
|
||||
dataSource.query.mockResolvedValueOnce([{
|
||||
metadata: { faydaVerified: true, faydaVerifiedAt: '2026-01-01T00:00:00.000Z' },
|
||||
name: { en: 'Test User', am: 'ቴስት ዩዘር' },
|
||||
}]);
|
||||
const result = await service.getVerificationStatus('iam-user-1');
|
||||
expect(result).toEqual({
|
||||
verified: true,
|
||||
verifiedAt: new Date('2026-01-01T00:00:00Z'),
|
||||
@@ -543,9 +464,9 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns verified=false when User row is missing or unverified', async () => {
|
||||
prisma.user.findUnique.mockResolvedValue(null);
|
||||
const result = await service.getVerificationStatus('user-x');
|
||||
it('returns verified=false when IAM user is missing or unverified', async () => {
|
||||
dataSource.query.mockResolvedValueOnce([]);
|
||||
const result = await service.getVerificationStatus('iam-user-x');
|
||||
expect(result).toEqual({ verified: false });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -250,50 +250,25 @@ export class VerifaydaService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Loads a user (+ relations) and mints the same JWT shape as `/auth/login`. */
|
||||
private async issueLoginToken(
|
||||
userId: string,
|
||||
_userId: string,
|
||||
): Promise<{ token: string; user: FaydaUserSummary }> {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { passenger: true, agent: true },
|
||||
});
|
||||
if (!user) {
|
||||
// Should not happen — we just resolved/created this user.
|
||||
throw new UnauthorizedException({
|
||||
code: 'FAYDA_LOGIN_FAILED',
|
||||
message: 'Could not load the verified user',
|
||||
});
|
||||
}
|
||||
|
||||
const summary: FaydaUserSummary = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
passengerId: user.passenger?.id,
|
||||
agentId: user.agent?.id,
|
||||
};
|
||||
|
||||
this.logger.warn(
|
||||
`Legacy passenger Fayda login reached for user ${user.id}; use IAM /v1/auth Fayda login to issue tokens.`,
|
||||
);
|
||||
throw new UnauthorizedException({
|
||||
code: 'FAYDA_LOGIN_MIGRATED_TO_IAM',
|
||||
message: 'Fayda login tokens are issued by the IAM package auth endpoints.',
|
||||
user: summary,
|
||||
});
|
||||
}
|
||||
|
||||
async getVerificationStatus(iamUserId: string): Promise<VerificationStatusDto> {
|
||||
const passenger = await this.prisma.passenger.findUnique({
|
||||
where: { iamUserId },
|
||||
include: { user: { select: { faydaVerified: true, faydaVerifiedAt: true, fullName: true } } },
|
||||
});
|
||||
return {
|
||||
verified: passenger?.user?.faydaVerified ?? false,
|
||||
verifiedAt: passenger?.user?.faydaVerifiedAt ?? undefined,
|
||||
fullName: passenger?.user?.fullName ?? undefined,
|
||||
};
|
||||
const rows = await this.dataSource.query<{ metadata: Record<string, any> | null; name: { en: string; am: string } | null }[]>(
|
||||
`SELECT metadata, name FROM iam.users WHERE id = $1 LIMIT 1`,
|
||||
[iamUserId],
|
||||
);
|
||||
const iam = rows[0] ?? null;
|
||||
const faydaVerified = iam?.metadata?.faydaVerified === true || iam?.metadata?.faydaVerified === 'true';
|
||||
const faydaVerifiedAt = iam?.metadata?.faydaVerifiedAt ? new Date(iam.metadata.faydaVerifiedAt) : undefined;
|
||||
const fullName = iam?.name?.en ?? iam?.name?.am ?? undefined;
|
||||
return { verified: faydaVerified, verifiedAt: faydaVerifiedAt, fullName };
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
|
||||
Reference in New Issue
Block a user