Skip to content

Commit 8f103a0

Browse files
gkrajniaknexus49gkrajniaksap
authored
feat: contentconfigurations grapqhl (#20)
* feat: add graphql gateway content configuration service provider and update app module * feat: refactor getServiceProviders method and improve base URL handling * feat: enhance content configuration service provider with validation and error handling * chore: fix the crd gateway calculation * chore: fix the crd gateway calculation * feat: integrate advanced request context and service providers updates - Implement `RequestContextProviderImpl` for enhanced context handling. - Add advanced `OpenmfpPortalContextService` and new providers. - Update to `@openmfp/portal-server-lib` version 0.156.0. - Refactor content configuration fetching with focused validations. - Upgrade dependencies and improve compatibility with Node.js 20+. * fix tests * fix tests --------- Co-authored-by: Bastian Echterhölter <[email protected]> Co-authored-by: Grzegorz Krajniak <[email protected]>
1 parent 206aa54 commit 8f103a0

15 files changed

+772
-583
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Mock implementation of @openmfp/portal-server-lib
2+
// eslint-disable-next-line no-undef
3+
module.exports = {
4+
EntityContextProvider: class EntityContextProvider {},
5+
EntityNotFoundException: class EntityNotFoundException extends Error {},
6+
HeaderParserService: class HeaderParserService {},
7+
EnvVariablesService: class EnvVariablesService {},
8+
EnvService: class EnvService {},
9+
EnvConfigVariables: class EnvConfigVariables {},
10+
DiscoveryService: class DiscoveryService {},
11+
PortalModule: class PortalModule {},
12+
};

backend/jest-custom-resolver.cjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const { resolve } = require('path');
2+
const { existsSync } = require('fs');
3+
4+
module.exports = (path, options) => {
5+
// If it's a .js import and the corresponding .ts file exists, resolve to .ts
6+
if (
7+
path.endsWith('.js') &&
8+
(path.startsWith('./') || path.startsWith('../'))
9+
) {
10+
const tsPath = path.replace(/\.js$/, '.ts');
11+
const fullTsPath = resolve(options.basedir, tsPath);
12+
if (existsSync(fullTsPath)) {
13+
return options.defaultResolver(tsPath, options);
14+
}
15+
}
16+
// Otherwise, use default resolution
17+
return options.defaultResolver(path, options);
18+
};

backend/jest.config.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module.exports = {
2+
testEnvironment: 'node',
3+
coverageReporters: ['text', 'cobertura', 'lcov'],
4+
transform: {
5+
'^.+\\.(t|j)s$': 'ts-jest',
6+
},
7+
rootDir: 'src',
8+
testRegex: '.spec.ts$',
9+
collectCoverageFrom: ['**/*.(t|j)s'],
10+
coverageDirectory: './coverage',
11+
resolver: '<rootDir>/../jest-custom-resolver.cjs',
12+
moduleNameMapper: {
13+
'^@openmfp/portal-server-lib$':
14+
'<rootDir>/../__mocks__/@openmfp/portal-server-lib.js',
15+
},
16+
};

backend/package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"@nestjs/core": "^10.4.6",
2929
"@nestjs/platform-express": "^10.4.6",
3030
"@nestjs/serve-static": "^4.0.0",
31-
"@openmfp/portal-server-lib": "0.155.0",
31+
"@openmfp/portal-server-lib": "0.156.0",
3232
"axios": "^1.6.3",
3333
"class-validator": "^0.14.1",
3434
"cookie-parser": "1.4.7",
@@ -64,34 +64,5 @@
6464
"tsconfig-paths": "^4.2.0",
6565
"typescript": "^5.1.3"
6666
},
67-
"jest": {
68-
"preset": "ts-jest/presets/default-esm",
69-
"extensionsToTreatAsEsm": [".ts"],
70-
"moduleFileExtensions": [
71-
"js",
72-
"json",
73-
"ts"
74-
],
75-
"rootDir": ".",
76-
"testRegex": ".*\\.spec\\.ts$",
77-
"transform": {
78-
"^.+\\.(t|j)s$": ["ts-jest", {
79-
"useESM": true
80-
}]
81-
},
82-
"collectCoverageFrom": [
83-
"**/*.(t|j)s"
84-
],
85-
"coverageDirectory": "./coverage",
86-
"testEnvironment": "node",
87-
"passWithNoTests": true,
88-
"roots": [
89-
"<rootDir>/src/"
90-
],
91-
"moduleNameMapper": {
92-
"^@openmfp/portal-lib(|/.*)$": "<rootDir>/libs/portal-lib/src/$1",
93-
"^(\\.{1,2}/.*)\\.js$": "$1"
94-
}
95-
},
9667
"type": "module"
9768
}

backend/src/app.module.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,36 @@
1-
import {AccountEntityContextProvider} from './entity-context-provider/account-entity-context-provider.service.js';
2-
import {Module} from '@nestjs/common';
3-
import {PortalModule, PortalModuleOptions} from '@openmfp/portal-server-lib';
4-
import {config} from 'dotenv';
1+
import { Module } from '@nestjs/common';
2+
import { PortalModule, PortalModuleOptions } from '@openmfp/portal-server-lib';
53
import * as path from 'node:path';
6-
import {
7-
ContentConfigurationServiceProvidersService
8-
} from "./service-providers/content-configuration-service-providers.service.js";
4+
import { AccountEntityContextProvider } from './entity-context-provider/account-entity-context-provider.service.js';
5+
import { OpenmfpPortalContextService } from './portal-context-provider/openmfp-portal-context.service.js';
6+
import { RequestContextProviderImpl } from './request-context-provider/openmfp-request-context-provider.js';
7+
import { ContentConfigurationServiceProvidersService } from './service-providers/content-configuration-service-providers.service.js';
8+
import { config } from 'dotenv';
99

1010
const __filename = new URL(import.meta.url).pathname;
1111
const __dirname = path.dirname(__filename);
1212

13-
config({path: './.env'});
13+
config({ path: './.env' });
1414

1515
const portalOptions: PortalModuleOptions = {
16-
frontendDistSources: path.join(
17-
__dirname,
18-
'../..',
19-
'frontend/dist/frontend/browser',
20-
),
21-
entityContextProviders: {
22-
account: AccountEntityContextProvider,
23-
},
24-
additionalProviders: [AccountEntityContextProvider],
25-
serviceProviderService: ContentConfigurationServiceProvidersService,
16+
frontendDistSources: path.join(
17+
__dirname,
18+
'../..',
19+
'frontend/dist/frontend/browser'
20+
),
21+
requestContextProvider: RequestContextProviderImpl,
22+
portalContextProvider: OpenmfpPortalContextService,
23+
entityContextProviders: {
24+
account: AccountEntityContextProvider,
25+
},
26+
additionalProviders: [
27+
AccountEntityContextProvider,
28+
OpenmfpPortalContextService,
29+
],
30+
serviceProviderService: ContentConfigurationServiceProvidersService,
2631
};
2732

2833
@Module({
29-
imports: [PortalModule.create(portalOptions)],
34+
imports: [PortalModule.create(portalOptions)],
3035
})
31-
export class AppModule {
32-
}
36+
export class AppModule {}

backend/src/entity-context-provider/account-entity-context-provider.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export class AccountEntityContextProvider implements EntityContextProvider {
2020
'gardener_project_list',
2121
'gardener_shoot_create',
2222
'gardener_shoot_list',
23+
'iamAdmin',
24+
'projectAdmin',
25+
'projectMember',
26+
'providerAdmin',
2327
],
2428
};
2529
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { Request } from 'express';
3+
import { EnvService } from '@openmfp/portal-server-lib';
4+
import { OpenmfpPortalContextService } from './openmfp-portal-context.service.js';
5+
6+
describe('OpenmfpPortalContextService', () => {
7+
let service: OpenmfpPortalContextService;
8+
let envService: jest.Mocked<EnvService>;
9+
let mockRequest: any;
10+
11+
beforeEach(async () => {
12+
const envServiceMock = {
13+
getDomain: jest.fn(),
14+
};
15+
16+
const module: TestingModule = await Test.createTestingModule({
17+
providers: [
18+
OpenmfpPortalContextService,
19+
{
20+
provide: EnvService,
21+
useValue: envServiceMock,
22+
},
23+
],
24+
}).compile();
25+
26+
service = module.get<OpenmfpPortalContextService>(
27+
OpenmfpPortalContextService
28+
);
29+
envService = module.get(EnvService);
30+
31+
mockRequest = {
32+
hostname: 'test.example.com',
33+
};
34+
35+
jest.spyOn(console, 'log').mockImplementation();
36+
});
37+
38+
afterEach(() => {
39+
jest.restoreAllMocks();
40+
});
41+
42+
it('should be defined', () => {
43+
expect(service).toBeDefined();
44+
});
45+
46+
it('should return empty context when no environment variables match prefix', async () => {
47+
envService.getDomain.mockReturnValue({
48+
domain: 'example.com',
49+
idpName: 'test-org',
50+
});
51+
52+
const result = await service.getContextValues(mockRequest as Request);
53+
54+
expect(result).toEqual({});
55+
});
56+
57+
it('should process environment variables with correct prefix', async () => {
58+
process.env.OPENMFP_PORTAL_CONTEXT_TEST_KEY = 'test-value';
59+
process.env.OPENMFP_PORTAL_CONTEXT_ANOTHER_TEST_KEY = 'another-value';
60+
process.env.OTHER_ENV_VAR = 'should-be-ignored';
61+
62+
try {
63+
envService.getDomain.mockReturnValue({
64+
domain: 'example.com',
65+
idpName: 'test-org',
66+
});
67+
68+
const result = await service.getContextValues(mockRequest as Request);
69+
70+
expect(result).toEqual({
71+
testKey: 'test-value',
72+
anotherTestKey: 'another-value',
73+
});
74+
} finally {
75+
delete process.env.OPENMFP_PORTAL_CONTEXT_TEST_KEY;
76+
delete process.env.OPENMFP_PORTAL_CONTEXT_ANOTHER_TEST_KEY;
77+
delete process.env.OTHER_ENV_VAR;
78+
}
79+
});
80+
81+
it('should convert snake_case to camelCase', async () => {
82+
process.env.OPENMFP_PORTAL_CONTEXT_SNAKE_CASE_KEY = 'value';
83+
process.env.OPENMFP_PORTAL_CONTEXT_MULTIPLE_SNAKE_CASE_KEYS = 'value2';
84+
85+
try {
86+
envService.getDomain.mockReturnValue({
87+
domain: 'example.com',
88+
idpName: 'test-org',
89+
});
90+
91+
const result = await service.getContextValues(mockRequest as Request);
92+
93+
expect(result).toEqual({
94+
snakeCaseKey: 'value',
95+
multipleSnakeCaseKeys: 'value2',
96+
});
97+
} finally {
98+
delete process.env.OPENMFP_PORTAL_CONTEXT_SNAKE_CASE_KEY;
99+
delete process.env.OPENMFP_PORTAL_CONTEXT_MULTIPLE_SNAKE_CASE_KEYS;
100+
}
101+
});
102+
103+
it('should process GraphQL gateway API URL with subdomain when hostname differs from domain', async () => {
104+
process.env.OPENMFP_PORTAL_CONTEXT_CRD_GATEWAY_API_URL =
105+
'https://${org-subdomain}api.example.com/${org-name}/graphql';
106+
107+
try {
108+
envService.getDomain.mockReturnValue({
109+
domain: 'example.com',
110+
idpName: 'test-org',
111+
});
112+
113+
mockRequest.hostname = 'subdomain.example.com';
114+
115+
const result = await service.getContextValues(mockRequest as Request);
116+
117+
expect(result.crdGatewayApiUrl).toBe(
118+
'https://test-org.api.example.com/test-org/graphql'
119+
);
120+
} finally {
121+
delete process.env.OPENMFP_PORTAL_CONTEXT_CRD_GATEWAY_API_URL;
122+
}
123+
});
124+
125+
it('should process GraphQL gateway API URL without subdomain when hostname matches domain', async () => {
126+
process.env.OPENMFP_PORTAL_CONTEXT_CRD_GATEWAY_API_URL =
127+
'https://${org-subdomain}api.example.com/${org-name}/graphql';
128+
129+
try {
130+
envService.getDomain.mockReturnValue({
131+
domain: 'example.com',
132+
idpName: 'test-org',
133+
});
134+
135+
mockRequest.hostname = 'example.com';
136+
137+
const result = await service.getContextValues(mockRequest as Request);
138+
139+
expect(result.crdGatewayApiUrl).toBe(
140+
'https://api.example.com/test-org/graphql'
141+
);
142+
} finally {
143+
delete process.env.OPENMFP_PORTAL_CONTEXT_CRD_GATEWAY_API_URL;
144+
}
145+
});
146+
147+
it('should ignore keys with empty names after trimming', async () => {
148+
process.env.OPENMFP_PORTAL_CONTEXT_ = 'should-be-ignored';
149+
process.env['OPENMFP_PORTAL_CONTEXT_ '] = 'should-also-be-ignored';
150+
process.env.OPENMFP_PORTAL_CONTEXT_VALID_KEY = 'valid-value';
151+
152+
try {
153+
envService.getDomain.mockReturnValue({
154+
domain: 'example.com',
155+
idpName: 'test-org',
156+
});
157+
158+
const result = await service.getContextValues(mockRequest as Request);
159+
160+
expect(result).toEqual({
161+
validKey: 'valid-value',
162+
});
163+
} finally {
164+
delete process.env.OPENMFP_PORTAL_CONTEXT_;
165+
delete process.env['OPENMFP_PORTAL_CONTEXT_ '];
166+
delete process.env.OPENMFP_PORTAL_CONTEXT_VALID_KEY;
167+
}
168+
});
169+
170+
it('should handle undefined crdGatewayApiUrl gracefully', async () => {
171+
process.env.OPENMFP_PORTAL_CONTEXT_OTHER_KEY = 'value';
172+
173+
try {
174+
envService.getDomain.mockReturnValue({
175+
domain: 'example.com',
176+
idpName: 'test-org',
177+
});
178+
179+
const result = await service.getContextValues(mockRequest as Request);
180+
181+
expect(result).toEqual({
182+
otherKey: 'value',
183+
});
184+
} finally {
185+
delete process.env.OPENMFP_PORTAL_CONTEXT_OTHER_KEY;
186+
}
187+
});
188+
});

0 commit comments

Comments
 (0)