Skip to content

Commit 1c994bb

Browse files
makdenissmakdeniss
authored andcommitted
feat: adjust jest.config and fix test converage (#2)
1 parent 784c71d commit 1c994bb

File tree

4 files changed

+218
-6
lines changed

4 files changed

+218
-6
lines changed

jest.config.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ module.exports = {
77
rootDir: '.',
88
testRegex: '.*\\.spec\\.ts$',
99
transform: {
10-
'^.+\\.(t|j)s$': 'ts-jest',
10+
'^.+\\.(t|j)s$': [
11+
'ts-jest',
12+
{
13+
tsconfig: 'tsconfig.test.json',
14+
useESM: true,
15+
},
16+
],
1117
},
1218
collectCoverageFrom: ['**/*.(t|j)s'],
1319
coverageDirectory: './coverage',
@@ -20,11 +26,6 @@ module.exports = {
2026
},
2127
preset: 'ts-jest/presets/default-esm',
2228
extensionsToTreatAsEsm: ['.ts'],
23-
globals: {
24-
'ts-jest': {
25-
useESM: true,
26-
},
27-
},
2829
transformIgnorePatterns: [
2930
'/node_modules/(?!(@openmfp/portal-server-lib|graphql-request)/)',
3031
],
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/iam/auth-callback-provider.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,30 @@ describe('AuthCallbackProvider', () => {
3939
expect(iamServiceMock.addUser).toHaveBeenCalledTimes(1);
4040
expect(iamServiceMock.addUser).toHaveBeenCalledWith('idtoken', req);
4141
});
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+
});
4268
});
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { KubernetesServiceProvidersService } from './kubernetes-service-providers.service';
2+
3+
const listClusterCustomObject = jest.fn();
4+
5+
jest.mock('@kubernetes/client-node', () => {
6+
class KubeConfig {
7+
loadFromDefault = jest.fn();
8+
getCurrentCluster = jest
9+
.fn()
10+
.mockReturnValue({ server: 'https://k8s.example.com/base' });
11+
makeApiClient = jest.fn().mockImplementation(() => ({
12+
listClusterCustomObject,
13+
}));
14+
}
15+
class CustomObjectsApi {}
16+
return { KubeConfig, CustomObjectsApi };
17+
});
18+
19+
jest.mock('@kubernetes/client-node/dist/gen/middleware.js', () => ({
20+
PromiseMiddlewareWrapper: class {
21+
pre?: (ctx: any) => Promise<any> | any;
22+
post?: (ctx: any) => Promise<any> | any;
23+
constructor(opts: any) {
24+
this.pre = opts.pre;
25+
this.post = opts.post;
26+
}
27+
},
28+
}));
29+
30+
describe('KubernetesServiceProvidersService', () => {
31+
beforeEach(() => {
32+
jest.resetAllMocks();
33+
});
34+
35+
it('should return empty list when API returns no items', async () => {
36+
listClusterCustomObject.mockImplementation(
37+
async (_gvr: any, _opts: any) => {
38+
return {};
39+
},
40+
);
41+
42+
const svc = new KubernetesServiceProvidersService();
43+
const res = await svc.getServiceProviders('token', [], {
44+
organization: 'org',
45+
});
46+
expect(res.rawServiceProviders).toEqual([]);
47+
});
48+
49+
it('should map items to contentConfiguration and fill url from spec when missing', async () => {
50+
let capturedUrl = '';
51+
listClusterCustomObject.mockImplementation(async (_gvr: any, opts: any) => {
52+
const mw = opts?.middleware?.[0];
53+
const ctx = {
54+
_url: 'https://k8s.example.com/base',
55+
getUrl() {
56+
return this._url;
57+
},
58+
setUrl(u: string) {
59+
this._url = u;
60+
},
61+
};
62+
if (mw?.pre) await mw.pre(ctx);
63+
capturedUrl = ctx._url;
64+
return {
65+
items: [
66+
{
67+
status: { configurationResult: JSON.stringify({}) },
68+
spec: {
69+
remoteConfiguration: { url: 'http://fallback.example/app' },
70+
},
71+
},
72+
],
73+
};
74+
});
75+
76+
const svc = new KubernetesServiceProvidersService();
77+
const res = await svc.getServiceProviders('token', ['main'], {
78+
organization: 'acme',
79+
account: 'a1',
80+
});
81+
82+
expect(res.rawServiceProviders[0].contentConfiguration).toHaveLength(1);
83+
expect(res.rawServiceProviders[0].contentConfiguration[0].url).toBe(
84+
'http://fallback.example/app',
85+
);
86+
87+
expect(capturedUrl).toContain(
88+
'/clusters/root:orgs:acme:a1/apis/core.openmfp.io/v1alpha1/contentconfigurations',
89+
);
90+
});
91+
92+
it('should retry once on HTTP 429 and log retry message', async () => {
93+
jest.useFakeTimers();
94+
const sequence: any[] = [
95+
Object.assign(new Error('Too Many Requests'), { code: 429 }),
96+
{ items: [] },
97+
];
98+
listClusterCustomObject.mockImplementation(async () => {
99+
const next = sequence.shift();
100+
if (next instanceof Error || next?.code === 429) {
101+
throw next;
102+
}
103+
return next;
104+
});
105+
106+
const logSpy = jest
107+
.spyOn(console, 'log')
108+
.mockImplementation(() => undefined as unknown as never);
109+
const errSpy = jest
110+
.spyOn(console, 'error')
111+
.mockImplementation(() => undefined as unknown as never);
112+
113+
const svc = new KubernetesServiceProvidersService();
114+
const promise = svc.getServiceProviders('token', [], {
115+
organization: 'org',
116+
});
117+
118+
await jest.advanceTimersByTimeAsync(1000);
119+
120+
const res = await promise;
121+
expect(res.rawServiceProviders).toEqual([
122+
{
123+
name: 'openmfp-system',
124+
displayName: '',
125+
creationTimestamp: '',
126+
contentConfiguration: [],
127+
},
128+
]);
129+
expect(logSpy).toHaveBeenCalledWith(
130+
'Retry after 1 second reading kubernetes resources.',
131+
);
132+
133+
logSpy.mockRestore();
134+
errSpy.mockRestore();
135+
jest.useRealTimers();
136+
});
137+
});

0 commit comments

Comments
 (0)