Skip to content

Commit 6b5eb01

Browse files
authored
Merge pull request #6 from platform-mesh/feat/onboard-user-iam-log-action
Feat/onboard user iam log action
2 parents 7a7fecb + f90c507 commit 6b5eb01

10 files changed

+398
-2
lines changed

jest.config.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,24 @@ module.exports = {
1717
},
1818
coveragePathIgnorePatterns: ['/node_modules/', '/integration-tests/'],
1919
coverageDirectory: '../test-run-reports/coverage/unit',
20-
transformIgnorePatterns: ['node_modules/(?!@openmfp/portal-server-lib/)'],
20+
transformIgnorePatterns: ['/node_modules/(?!(@openmfp/portal-server-lib|graphql-request)/)'],
21+
transform: {
22+
'^.+\\.(t|j)s$': [
23+
'ts-jest',
24+
{
25+
tsconfig: 'tsconfig.test.json',
26+
useESM: true,
27+
},
28+
],
29+
},
30+
testEnvironment: 'node',
31+
passWithNoTests: true,
32+
roots: ['<rootDir>'],
33+
moduleNameMapper: {
34+
'^@openmfp/portal-lib(|/.*)$': '<rootDir>/libs/portal-lib/src/$1',
35+
'^(\\.{1,2}/.*)\\.js$': '$1',
36+
},
37+
preset: 'ts-jest/presets/default-esm',
38+
extensionsToTreatAsEsm: ['.ts'],
39+
moduleFileExtensions: ['js', 'json', 'ts'],
2140
};

src/portal-options/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './entity-context-provider/account-entity-context-provider.service
22
export * from './portal-context-provider/openmfp-portal-context.service.js';
33
export * from './request-context-provider/openmfp-request-context-provider.js';
44
export * from './service-providers/content-configuration-service-providers.service.js';
5+
export * from './service-providers/iam/auth-callback-provider.js';
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { OpenmfpPortalContextService } from '../portal-context-provider/openmfp-portal-context.service';
2+
import { RequestContextProviderImpl } from './openmfp-request-context-provider';
3+
import { EnvService } from '@openmfp/portal-server-lib';
4+
import type { Request } from 'express';
5+
import { mock } from 'jest-mock-extended';
6+
7+
describe('RequestContextProviderImpl', () => {
8+
let provider: RequestContextProviderImpl;
9+
const envService = mock<EnvService>();
10+
const portalContext = mock<OpenmfpPortalContextService>();
11+
12+
beforeEach(() => {
13+
jest.resetAllMocks();
14+
(envService.getDomain as unknown as jest.Mock).mockReturnValue({
15+
idpName: 'org1',
16+
domain: 'org1.example.com',
17+
});
18+
(portalContext.getContextValues as unknown as jest.Mock).mockResolvedValue({
19+
crdGatewayApiUrl: 'http://gateway/graphql',
20+
other: 'x',
21+
});
22+
23+
provider = new RequestContextProviderImpl(
24+
envService as any,
25+
portalContext as any,
26+
);
27+
});
28+
29+
it('should merge request query, portal context and organization from envService', async () => {
30+
const req = {
31+
query: { account: 'acc-123', extra: '1' },
32+
hostname: 'org1.example.com',
33+
} as unknown as Request;
34+
35+
const result = await provider.getContextValues(req);
36+
37+
expect(result).toMatchObject({
38+
account: 'acc-123',
39+
extra: '1',
40+
crdGatewayApiUrl: 'http://gateway/graphql',
41+
other: 'x',
42+
organization: 'org1',
43+
});
44+
45+
expect(envService.getDomain).toHaveBeenCalledWith(req);
46+
expect(portalContext.getContextValues).toHaveBeenCalledWith(req);
47+
});
48+
});

src/portal-options/service-providers/content-configuration-service-providers.service.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ jest.mock('graphql-request', () => {
77
GraphQLClient: jest.fn().mockImplementation(() => ({
88
request: jest.fn(),
99
})),
10-
gql(query: TemplateStringsArray) { return query[0]; },
10+
gql(query: TemplateStringsArray) {
11+
return query[0];
12+
},
1113
};
1214
});
1315

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { AuthCallbackProvider } from './auth-callback-provider';
2+
import { IAMGraphQlService } from './iam-graphql.service';
3+
import { Test, TestingModule } from '@nestjs/testing';
4+
import type { AuthTokenData } from '@openmfp/portal-server-lib';
5+
import type { Request, Response } from 'express';
6+
import { mock } from 'jest-mock-extended';
7+
8+
describe('AuthCallbackProvider', () => {
9+
let callback: AuthCallbackProvider;
10+
let iamServiceMock: IAMGraphQlService;
11+
12+
beforeEach(async () => {
13+
iamServiceMock = mock<IAMGraphQlService>();
14+
const module: TestingModule = await Test.createTestingModule({
15+
providers: [
16+
AuthCallbackProvider,
17+
{
18+
provide: IAMGraphQlService,
19+
useValue: iamServiceMock,
20+
},
21+
],
22+
}).compile();
23+
24+
callback = module.get<AuthCallbackProvider>(AuthCallbackProvider);
25+
});
26+
27+
it('should be defined', () => {
28+
expect(callback).toBeDefined();
29+
});
30+
31+
it('should create a user', async () => {
32+
const req = mock<Request>();
33+
const res = mock<Response>();
34+
35+
await callback.handleSuccess(req, res, {
36+
id_token: 'idtoken',
37+
} as AuthTokenData);
38+
39+
expect(iamServiceMock.addUser).toHaveBeenCalledTimes(1);
40+
expect(iamServiceMock.addUser).toHaveBeenCalledWith('idtoken', req);
41+
});
42+
43+
it('should log error if addUser throws', async () => {
44+
const req = mock<Request>();
45+
const res = mock<Response>();
46+
const error = new Error('boom');
47+
(iamServiceMock.addUser as jest.Mock).mockRejectedValueOnce(error);
48+
49+
const errorSpy = jest
50+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
51+
.spyOn((callback as any).logger, 'error')
52+
.mockImplementation(() => undefined as unknown as never);
53+
54+
await callback.handleSuccess(req, res, {
55+
id_token: 'bad',
56+
} as AuthTokenData);
57+
58+
expect(iamServiceMock.addUser).toHaveBeenCalledTimes(1);
59+
expect(errorSpy).toHaveBeenCalledWith(error);
60+
});
61+
62+
it('should resolve handleFailure without action', async () => {
63+
const req = mock<Request>();
64+
const res = mock<Response>();
65+
66+
await expect(callback.handleFailure(req, res)).resolves.toBeUndefined();
67+
});
68+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { IAMGraphQlService } from './iam-graphql.service.js';
2+
import { Injectable, Logger } from '@nestjs/common';
3+
import { AuthCallback, AuthTokenData } from '@openmfp/portal-server-lib';
4+
import { Request, Response } from 'express';
5+
6+
@Injectable()
7+
export class AuthCallbackProvider implements AuthCallback {
8+
private logger: Logger = new Logger(AuthCallbackProvider.name);
9+
10+
constructor(private iamService: IAMGraphQlService) {}
11+
12+
async handleSuccess(
13+
request: Request,
14+
response: Response,
15+
authTokenResponse: AuthTokenData,
16+
): Promise<void> {
17+
try {
18+
await this.iamService.addUser(authTokenResponse.id_token, request);
19+
} catch (e) {
20+
this.logger.error(e);
21+
}
22+
}
23+
24+
async handleFailure(request: Request, response: Response): Promise<void> {
25+
return Promise.resolve();
26+
}
27+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { RequestContextProviderImpl } from '../../request-context-provider/openmfp-request-context-provider';
2+
import { IAMGraphQlService } from './iam-graphql.service';
3+
import { MUTATION_LOGIN } from './queries';
4+
import { GraphQLClient } from 'graphql-request';
5+
import { mock } from 'jest-mock-extended';
6+
7+
describe('IAMGraphQlService', () => {
8+
const mockIamServiceApiUrl = 'http://localhost:8080/query';
9+
let service: IAMGraphQlService;
10+
const gqlClient = {
11+
request: jest.fn(),
12+
} as unknown as GraphQLClient;
13+
14+
const requestContextProvider = mock<RequestContextProviderImpl>({
15+
getContextValues: jest
16+
.fn()
17+
.mockResolvedValue({ iamServiceApiUrl: mockIamServiceApiUrl }),
18+
});
19+
20+
let GraphQLClientMock: jest.SpyInstance;
21+
22+
beforeEach(() => {
23+
jest.resetAllMocks();
24+
25+
// Re-apply RequestContextProvider mock after resetAllMocks
26+
(
27+
requestContextProvider.getContextValues as unknown as jest.Mock
28+
).mockResolvedValue({
29+
iamServiceApiUrl: mockIamServiceApiUrl,
30+
});
31+
32+
// Mock GraphQLClient constructor to return our mocked client
33+
GraphQLClientMock = jest
34+
.spyOn<any, any>(require('graphql-request'), 'GraphQLClient')
35+
.mockImplementation(() => gqlClient);
36+
37+
service = new IAMGraphQlService(requestContextProvider as any);
38+
});
39+
40+
it('should call mutation addUser', async () => {
41+
(gqlClient.request as jest.Mock).mockResolvedValue('');
42+
43+
const response = await service.addUser('token', {} as any);
44+
45+
expect(GraphQLClientMock).toHaveBeenCalledWith(mockIamServiceApiUrl, {
46+
headers: { Authorization: 'Bearer token' },
47+
});
48+
expect(gqlClient.request).toHaveBeenCalledWith(MUTATION_LOGIN);
49+
expect(response).toBe(undefined);
50+
});
51+
52+
it('should call mutation addUser and log error', async () => {
53+
console.error = jest.fn();
54+
(gqlClient.request as jest.Mock).mockRejectedValue('error');
55+
56+
const response = await service.addUser('token', {} as any);
57+
expect(response).toBe(undefined);
58+
expect(console.error).toHaveBeenCalledWith('error');
59+
});
60+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { RequestContextProviderImpl } from '../../request-context-provider/openmfp-request-context-provider';
2+
import { MUTATION_LOGIN } from './queries.js';
3+
import { Injectable } from '@nestjs/common';
4+
import type { Request } from 'express';
5+
import { GraphQLClient } from 'graphql-request';
6+
7+
@Injectable()
8+
export class IAMGraphQlService {
9+
constructor(private requestContextProvider: RequestContextProviderImpl) {}
10+
11+
async addUser(token: string, request: Request): Promise<void> {
12+
const requestContext =
13+
await this.requestContextProvider.getContextValues(request);
14+
const iamUrl = requestContext.iamServiceApiUrl;
15+
const client = new GraphQLClient(iamUrl, {
16+
headers: {
17+
Authorization: `Bearer ${token}`,
18+
},
19+
});
20+
21+
try {
22+
const response = await client.request(MUTATION_LOGIN);
23+
} catch (e) {
24+
console.error(e);
25+
}
26+
}
27+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { gql } from 'graphql-request';
2+
3+
export const MUTATION_LOGIN = gql`
4+
mutation {
5+
login
6+
}
7+
`;

0 commit comments

Comments
 (0)