Skip to content

Commit 84c8e54

Browse files
committed
test: add middleware tests
1 parent df3625d commit 84c8e54

File tree

10 files changed

+918
-3
lines changed

10 files changed

+918
-3
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
test
2+
dist
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = {
2+
extends: ['../../.eslintrc'],
3+
parserOptions: {
4+
project: './tsconfig.eslint.json',
5+
tsconfigRootDir: __dirname
6+
}
7+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const config = require('../../jest.base');
2+
module.exports = config;

packages/backend-proxy-middleware-cf/src/middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ module.exports = async ({ options }: MiddlewareParameters<CfOAuthMiddlewareConfi
2323
transports: [new UI5ToolingTransport({ moduleName: 'backend-proxy-middleware-cf' })]
2424
});
2525

26-
validateConfig(config, logger);
26+
await validateConfig(config, logger);
2727

2828
const tokenProvider = await createTokenProvider(config, logger);
2929
const router = setupProxyRoutes(config.paths, config.url, tokenProvider, logger);

packages/backend-proxy-middleware-cf/src/token/factory.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ export function createManagerFromServiceKeys(serviceKeys: ServiceKeys, logger: T
3333
throw new Error('Invalid credentials: missing UAA URL');
3434
}
3535

36-
if (!uaa.clientid) {
36+
if (!uaa?.clientid) {
3737
throw new Error('Invalid credentials: missing client ID');
3838
}
3939

40-
if (!uaa.clientsecret) {
40+
if (!uaa?.clientsecret) {
4141
throw new Error('Invalid credentials: missing client secret');
4242
}
4343

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import express from 'express';
2+
import supertest from 'supertest';
3+
import type { ToolsLogger } from '@sap-ux/logger';
4+
5+
import * as proxy from '../src/proxy';
6+
import * as middleware from '../src/middleware';
7+
import * as validation from '../src/validation';
8+
import * as tokenFactory from '../src/token/factory';
9+
import type { CfOAuthMiddlewareConfig } from '../src/types';
10+
11+
jest.mock('../src/proxy');
12+
jest.mock('../src/validation');
13+
jest.mock('../src/token/factory');
14+
jest.mock('@sap-ux/adp-tooling', () => ({
15+
...jest.requireActual('@sap-ux/adp-tooling'),
16+
isLoggedInCf: jest.fn().mockResolvedValue(true),
17+
loadCfConfig: jest.fn().mockReturnValue({})
18+
}));
19+
20+
jest.mock('@sap-ux/logger', () => ({
21+
...jest.requireActual('@sap-ux/logger'),
22+
ToolsLogger: jest.fn().mockReturnValue({
23+
debug: jest.fn(),
24+
info: jest.fn(),
25+
error: jest.fn(),
26+
warn: jest.fn()
27+
} as unknown as ToolsLogger)
28+
}));
29+
30+
const mockSetupProxyRoutes = proxy.setupProxyRoutes as jest.Mock;
31+
const mockValidateConfig = validation.validateConfig as jest.Mock;
32+
const mockCreateTokenProvider = tokenFactory.createTokenProvider as jest.Mock;
33+
34+
async function getTestServer(configuration: CfOAuthMiddlewareConfig): Promise<supertest.SuperTest<supertest.Test>> {
35+
const router = await (middleware as any).default({
36+
options: { configuration }
37+
});
38+
const app = express();
39+
app.use(router);
40+
return supertest(app);
41+
}
42+
43+
describe('backend-proxy-middleware-cf', () => {
44+
const mockTokenProvider = {
45+
createTokenMiddleware: jest.fn().mockReturnValue((req: any, res: any, next: any) => next())
46+
};
47+
48+
beforeEach(() => {
49+
jest.clearAllMocks();
50+
mockValidateConfig.mockResolvedValue(undefined);
51+
mockCreateTokenProvider.mockResolvedValue(mockTokenProvider);
52+
mockSetupProxyRoutes.mockReturnValue(express.Router());
53+
});
54+
55+
describe('Middleware initialization', () => {
56+
const baseConfig: CfOAuthMiddlewareConfig = {
57+
url: '/backend.example',
58+
paths: ['/sap/opu/odata']
59+
};
60+
61+
test('minimal configuration', async () => {
62+
await getTestServer(baseConfig);
63+
64+
expect(mockValidateConfig).toHaveBeenCalledWith(baseConfig, expect.any(Object));
65+
expect(mockCreateTokenProvider).toHaveBeenCalledWith(baseConfig, expect.any(Object));
66+
expect(mockSetupProxyRoutes).toHaveBeenCalledWith(
67+
baseConfig.paths,
68+
baseConfig.url,
69+
mockTokenProvider,
70+
expect.any(Object)
71+
);
72+
});
73+
74+
test('with debug enabled', async () => {
75+
const configWithDebug = { ...baseConfig, debug: true };
76+
await getTestServer(configWithDebug);
77+
78+
expect(mockValidateConfig).toHaveBeenCalledWith(configWithDebug, expect.any(Object));
79+
});
80+
81+
test('with credentials', async () => {
82+
const configWithCredentials: CfOAuthMiddlewareConfig = {
83+
...baseConfig,
84+
credentials: {
85+
clientId: 'test-client',
86+
clientSecret: 'test-secret',
87+
url: '/uaa.example'
88+
}
89+
};
90+
await getTestServer(configWithCredentials);
91+
92+
expect(mockCreateTokenProvider).toHaveBeenCalledWith(configWithCredentials, expect.any(Object));
93+
});
94+
95+
test('with multiple paths', async () => {
96+
const configWithMultiplePaths: CfOAuthMiddlewareConfig = {
97+
...baseConfig,
98+
paths: ['/sap/opu/odata', '/sap/bc/ui5_ui5', '/api']
99+
};
100+
await getTestServer(configWithMultiplePaths);
101+
102+
expect(mockSetupProxyRoutes).toHaveBeenCalledWith(
103+
configWithMultiplePaths.paths,
104+
configWithMultiplePaths.url,
105+
mockTokenProvider,
106+
expect.any(Object)
107+
);
108+
});
109+
110+
test('throws error when validation fails', async () => {
111+
const config: CfOAuthMiddlewareConfig = {
112+
url: '/backend.example',
113+
paths: ['/sap/opu/odata']
114+
};
115+
const validationError = new Error('Validation failed');
116+
mockValidateConfig.mockRejectedValueOnce(validationError);
117+
118+
await expect(getTestServer(config)).rejects.toThrow('Validation failed');
119+
});
120+
});
121+
});
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import nock from 'nock';
2+
import supertest from 'supertest';
3+
import express, { type Request, type Response, type NextFunction, Router } from 'express';
4+
5+
import type { ToolsLogger } from '@sap-ux/logger';
6+
7+
import type { OAuthTokenProvider } from '../src/token';
8+
import { createProxyOptions, registerProxyRoute, setupProxyRoutes } from '../src/proxy';
9+
10+
describe('proxy', () => {
11+
const logger = {
12+
debug: jest.fn(),
13+
info: jest.fn(),
14+
error: jest.fn(),
15+
warn: jest.fn()
16+
} as unknown as ToolsLogger;
17+
18+
const mockTokenProvider = {
19+
createTokenMiddleware: jest.fn().mockReturnValue((req: Request, _res: Response, next: NextFunction) => {
20+
req.headers.authorization = 'Bearer mock-token';
21+
next();
22+
})
23+
} as unknown as OAuthTokenProvider;
24+
25+
beforeEach(() => {
26+
jest.clearAllMocks();
27+
});
28+
29+
const targetUrl = '/backend.example';
30+
31+
describe('createProxyOptions', () => {
32+
test('creates proxy options with correct target', () => {
33+
const options = createProxyOptions(targetUrl, logger);
34+
35+
expect(options.target).toBe(targetUrl);
36+
expect(options.changeOrigin).toBe(true);
37+
expect(options.pathRewrite).toBeDefined();
38+
expect(options.on).toBeDefined();
39+
expect(options.on?.error).toBeDefined();
40+
});
41+
42+
test('pathRewrite uses originalUrl when available', () => {
43+
const options = createProxyOptions(targetUrl, logger);
44+
const pathRewrite = options.pathRewrite as Function;
45+
46+
const req = {
47+
originalUrl: '/sap/opu/odata/srv/EntitySet',
48+
url: '/srv/EntitySet'
49+
};
50+
51+
const result = pathRewrite('/srv/EntitySet', req);
52+
expect(result).toBe('/sap/opu/odata/srv/EntitySet');
53+
});
54+
55+
test('pathRewrite uses req.url when originalUrl is not available', () => {
56+
const options = createProxyOptions(targetUrl, logger);
57+
const pathRewrite = options.pathRewrite as Function;
58+
59+
const req = {
60+
url: '/srv/EntitySet'
61+
};
62+
63+
const result = pathRewrite('/srv/EntitySet', req);
64+
expect(result).toBe('/srv/EntitySet');
65+
});
66+
67+
test('pathRewrite preserves query string', () => {
68+
const options = createProxyOptions(targetUrl, logger);
69+
const pathRewrite = options.pathRewrite as Function;
70+
71+
const req = {
72+
originalUrl: '/sap/opu/odata/srv/EntitySet?$top=10&$skip=0',
73+
url: '/srv/EntitySet?$top=10&$skip=0'
74+
};
75+
76+
const result = pathRewrite('/srv/EntitySet?$top=10&$skip=0', req);
77+
expect(result).toBe('/sap/opu/odata/srv/EntitySet?$top=10&$skip=0');
78+
});
79+
80+
test('error handler logs error and calls next if available', () => {
81+
const options = createProxyOptions(targetUrl, logger);
82+
const errorHandler = options.on?.error as Function;
83+
84+
const error = new Error('Proxy error');
85+
const req = {
86+
originalUrl: '/sap/opu/odata',
87+
url: '/sap/opu/odata',
88+
next: jest.fn()
89+
};
90+
const res = {};
91+
const target = '/backend.example';
92+
93+
errorHandler(error, req, res, target);
94+
expect(req.next).toHaveBeenCalledWith(error);
95+
});
96+
97+
test('error handler does not throw if next is not available', () => {
98+
const options = createProxyOptions(targetUrl, logger);
99+
const errorHandler = options.on?.error as Function;
100+
101+
const error = new Error('Proxy error');
102+
const req = {
103+
originalUrl: '/sap/opu/odata',
104+
url: '/sap/opu/odata'
105+
};
106+
const res = {};
107+
108+
expect(() => errorHandler(error, req, res, targetUrl)).not.toThrow();
109+
});
110+
});
111+
112+
describe('registerProxyRoute', () => {
113+
test('registers proxy route successfully', () => {
114+
const router = Router();
115+
const path = '/sap/opu/odata';
116+
const destinationUrl = '/backend.example';
117+
118+
registerProxyRoute(path, destinationUrl, mockTokenProvider, logger, router);
119+
120+
expect(mockTokenProvider.createTokenMiddleware).toHaveBeenCalled();
121+
expect(router.stack.length).toBeGreaterThan(0);
122+
});
123+
});
124+
125+
describe('setupProxyRoutes', () => {
126+
test('sets up multiple proxy routes', () => {
127+
const paths = ['/sap/opu/odata', '/sap/bc/ui5_ui5', '/api'];
128+
const destinationUrl = '/backend.example';
129+
130+
const router = setupProxyRoutes(paths, destinationUrl, mockTokenProvider, logger);
131+
132+
expect(typeof router).toBe('function');
133+
expect(router.use).toBeDefined();
134+
expect(mockTokenProvider.createTokenMiddleware).toHaveBeenCalledTimes(paths.length);
135+
});
136+
137+
test('throws error when route registration fails', () => {
138+
const paths = ['/sap/opu/odata'];
139+
const destinationUrl = '/backend.example';
140+
const failingTokenProvider = {
141+
createTokenMiddleware: jest.fn().mockImplementation(() => {
142+
throw new Error('Token middleware creation failed');
143+
})
144+
} as unknown as OAuthTokenProvider;
145+
146+
expect(() => {
147+
setupProxyRoutes(paths, destinationUrl, failingTokenProvider, logger);
148+
}).toThrow('Failed to register proxy for /sap/opu/odata');
149+
});
150+
151+
test('handles empty paths array', () => {
152+
const paths: string[] = [];
153+
const destinationUrl = '/backend.example';
154+
155+
const router = setupProxyRoutes(paths, destinationUrl, mockTokenProvider, logger);
156+
157+
expect(typeof router).toBe('function');
158+
expect(router.use).toBeDefined();
159+
expect(router.stack.length).toBe(0);
160+
});
161+
});
162+
163+
describe('integration tests', () => {
164+
const path = '/sap/opu/odata';
165+
const destinationUrl = 'https://backend.example';
166+
167+
test('proxies request with token middleware', async () => {
168+
const router = setupProxyRoutes([path], destinationUrl, mockTokenProvider, logger);
169+
170+
const app = express();
171+
app.use(router);
172+
173+
nock(destinationUrl)
174+
.get(`${path}/EntitySet`)
175+
.matchHeader('authorization', 'Bearer mock-token')
176+
.reply(200, { value: [] });
177+
178+
const server = supertest(app);
179+
const response = await server.get(`${path}/EntitySet`);
180+
181+
expect(response.status).toBe(200);
182+
expect(response.body).toEqual({ value: [] });
183+
});
184+
185+
test('proxies request with query parameters', async () => {
186+
const router = setupProxyRoutes([path], destinationUrl, mockTokenProvider, logger);
187+
188+
const app = express();
189+
app.use(router);
190+
191+
nock(destinationUrl).get(`${path}/EntitySet`).query({ $top: '10', $skip: '0' }).reply(200, { value: [] });
192+
193+
const server = supertest(app);
194+
const response = await server.get(`${path}/EntitySet?$top=10&$skip=0`);
195+
196+
expect(response.status).toBe(200);
197+
});
198+
199+
test('handles non-proxied paths', async () => {
200+
const router = setupProxyRoutes([path], destinationUrl, mockTokenProvider, logger);
201+
202+
const app = express();
203+
app.use(router);
204+
205+
const server = supertest(app);
206+
const response = await server.get('/not/proxied/path');
207+
208+
expect(response.status).toBe(404);
209+
});
210+
});
211+
});

0 commit comments

Comments
 (0)