fix: improve endpoint parsing in parseExample method (#5762)
This commit is contained in:
parent
194c9496aa
commit
992579e285
3 changed files with 465 additions and 17 deletions
|
|
@ -67,8 +67,8 @@ services:
|
||||||
target: app
|
target: app
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
depends_on:
|
# depends_on:
|
||||||
- hoppscotch-backend
|
# - hoppscotch-backend
|
||||||
ports:
|
ports:
|
||||||
- "3080:80"
|
- "3080:80"
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
|
|
@ -86,8 +86,8 @@ services:
|
||||||
target: sh_admin
|
target: sh_admin
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
depends_on:
|
# depends_on:
|
||||||
- hoppscotch-backend
|
# - hoppscotch-backend
|
||||||
ports:
|
ports:
|
||||||
- "3280:80"
|
- "3280:80"
|
||||||
- "3100:3100"
|
- "3100:3100"
|
||||||
|
|
@ -177,7 +177,7 @@ services:
|
||||||
depends_on:
|
depends_on:
|
||||||
hoppscotch-db:
|
hoppscotch-db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
command: sh -c "pnpx prisma migrate deploy"
|
command: sh -c "pnpm exec prisma migrate deploy"
|
||||||
|
|
||||||
# All the services listed below are deprecated
|
# All the services listed below are deprecated
|
||||||
# These services are kept for backward compatibility but should not be used for new deployments
|
# These services are kept for backward compatibility but should not be used for new deployments
|
||||||
|
|
|
||||||
|
|
@ -1453,4 +1453,437 @@ describe('MockServerService', () => {
|
||||||
expect(hasAccess).toBe(false);
|
expect(hasAccess).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('parseExample (private method)', () => {
|
||||||
|
const requestId = 'req123';
|
||||||
|
|
||||||
|
test('should parse basic example with path only', () => {
|
||||||
|
const exampleData = {
|
||||||
|
key: 'example1',
|
||||||
|
name: 'Get Users',
|
||||||
|
method: 'GET',
|
||||||
|
endpoint: 'http://api.example.com/users',
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
responseBody: '{"success": true}',
|
||||||
|
responseHeaders: [{ key: 'content-type', value: 'application/json' }],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.id).toBe('example1');
|
||||||
|
expect(result.name).toBe('Get Users');
|
||||||
|
expect(result.method).toBe('GET');
|
||||||
|
expect(result.endpoint).toBe('http://api.example.com/users');
|
||||||
|
expect(result.path).toBe('/users');
|
||||||
|
expect(result.queryParams).toEqual({});
|
||||||
|
expect(result.statusCode).toBe(200);
|
||||||
|
expect(result.statusText).toBe('OK');
|
||||||
|
expect(result.responseBody).toBe('{"success": true}');
|
||||||
|
expect(result.responseHeaders).toHaveLength(1);
|
||||||
|
expect(result.requestHeaders).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse example with query parameters', () => {
|
||||||
|
const exampleData = {
|
||||||
|
key: 'example2',
|
||||||
|
name: 'Search Users',
|
||||||
|
method: 'GET',
|
||||||
|
endpoint: 'http://api.example.com/users?page=1&limit=10&sort=name',
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
responseBody: '[]',
|
||||||
|
responseHeaders: [],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.path).toBe('/users');
|
||||||
|
expect(result.queryParams).toEqual({
|
||||||
|
page: '1',
|
||||||
|
limit: '10',
|
||||||
|
sort: 'name',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse example with path variables', () => {
|
||||||
|
const exampleData = {
|
||||||
|
key: 'example3',
|
||||||
|
name: 'Get User By ID',
|
||||||
|
method: 'GET',
|
||||||
|
endpoint: 'http://api.example.com/users/<<userId>>',
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
responseBody: '{"id": "123"}',
|
||||||
|
responseHeaders: [],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.path).toBe('/users/<<userId>>');
|
||||||
|
expect(result.queryParams).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse example with path variables and query params', () => {
|
||||||
|
const exampleData = {
|
||||||
|
key: 'example4',
|
||||||
|
name: 'Update User',
|
||||||
|
method: 'PUT',
|
||||||
|
endpoint: 'http://api.example.com/users/<<userId>>?notify=true',
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
responseBody: '{"updated": true}',
|
||||||
|
responseHeaders: [],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.path).toBe('/users/<<userId>>');
|
||||||
|
expect(result.queryParams).toEqual({ notify: 'true' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle endpoint starting with <<', () => {
|
||||||
|
const exampleData = {
|
||||||
|
key: 'example5',
|
||||||
|
name: 'Dynamic Base',
|
||||||
|
method: 'GET',
|
||||||
|
endpoint: '<<baseUrl>>/api/users',
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
responseBody: '[]',
|
||||||
|
responseHeaders: [],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.path).toBe('/api/users');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle endpoint without domain', () => {
|
||||||
|
const exampleData = {
|
||||||
|
key: 'example6',
|
||||||
|
name: 'Relative Path',
|
||||||
|
method: 'POST',
|
||||||
|
endpoint: '/api/users',
|
||||||
|
statusCode: 201,
|
||||||
|
statusText: 'Created',
|
||||||
|
responseBody: '{"id": "new"}',
|
||||||
|
responseHeaders: [],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.path).toBe('/api/users');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove domain from endpoint', () => {
|
||||||
|
const exampleData = {
|
||||||
|
key: 'example7',
|
||||||
|
name: 'Full URL',
|
||||||
|
method: 'GET',
|
||||||
|
endpoint: 'https://subdomain.example.com/api/v1/users',
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
responseBody: '[]',
|
||||||
|
responseHeaders: [],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.path).toBe('/api/v1/users');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should use default values when fields are missing', () => {
|
||||||
|
const exampleData = {
|
||||||
|
endpoint: '/users',
|
||||||
|
responseBody: '{}',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.id).toBe(`${requestId}-undefined`);
|
||||||
|
expect(result.method).toBe('GET');
|
||||||
|
expect(result.statusCode).toBe(200);
|
||||||
|
expect(result.statusText).toBe('OK');
|
||||||
|
expect(result.responseHeaders).toEqual([]);
|
||||||
|
expect(result.requestHeaders).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate ID from requestId and name when key is missing', () => {
|
||||||
|
const exampleData = {
|
||||||
|
name: 'Test Example',
|
||||||
|
endpoint: '/test',
|
||||||
|
responseBody: '{}',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.id).toBe(`${requestId}-Test Example`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle complex path with multiple segments', () => {
|
||||||
|
const exampleData = {
|
||||||
|
key: 'example8',
|
||||||
|
name: 'Nested Resource',
|
||||||
|
method: 'GET',
|
||||||
|
endpoint:
|
||||||
|
'http://api.example.com/organizations/<<orgId>>/teams/<<teamId>>/members',
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
responseBody: '[]',
|
||||||
|
responseHeaders: [],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.path).toBe(
|
||||||
|
'/organizations/<<orgId>>/teams/<<teamId>>/members',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should preserve special characters in query parameters', () => {
|
||||||
|
const exampleData = {
|
||||||
|
key: 'example9',
|
||||||
|
name: 'Special Chars',
|
||||||
|
method: 'GET',
|
||||||
|
endpoint:
|
||||||
|
'http://api.example.com/search?q=hello+world&filter=name:john',
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
responseBody: '[]',
|
||||||
|
responseHeaders: [],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.queryParams.q).toBe('hello world');
|
||||||
|
expect(result.queryParams.filter).toBe('name:john');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle root path', () => {
|
||||||
|
const exampleData = {
|
||||||
|
key: 'example10',
|
||||||
|
name: 'Root',
|
||||||
|
method: 'GET',
|
||||||
|
endpoint: 'http://api.example.com/',
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
responseBody: '{"status": "ok"}',
|
||||||
|
responseHeaders: [],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.path).toBe('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle empty endpoint gracefully', () => {
|
||||||
|
const exampleData = {
|
||||||
|
key: 'example11',
|
||||||
|
name: 'Empty Endpoint',
|
||||||
|
method: 'GET',
|
||||||
|
endpoint: '',
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
responseBody: '{}',
|
||||||
|
responseHeaders: [],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.path).toBe('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle encoded characters in path', () => {
|
||||||
|
const exampleData = {
|
||||||
|
key: 'example12',
|
||||||
|
name: 'Encoded Path',
|
||||||
|
method: 'GET',
|
||||||
|
endpoint: 'http://api.example.com/users/%3C%3CuserId%3E%3E',
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
responseBody: '{}',
|
||||||
|
responseHeaders: [],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.path).toBe('/users/<<userId>>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle multiple query parameters with same key', () => {
|
||||||
|
const exampleData = {
|
||||||
|
key: 'example13',
|
||||||
|
name: 'Multiple Query Values',
|
||||||
|
method: 'GET',
|
||||||
|
endpoint: 'http://api.example.com/users?id=1&id=2&id=3',
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
responseBody: '[]',
|
||||||
|
responseHeaders: [],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
// URLSearchParams keeps last value when keys duplicate
|
||||||
|
expect(result.queryParams.id).toBe('3');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle POST method with request body', () => {
|
||||||
|
const exampleData = {
|
||||||
|
key: 'example14',
|
||||||
|
name: 'Create User',
|
||||||
|
method: 'POST',
|
||||||
|
endpoint: 'http://api.example.com/users',
|
||||||
|
statusCode: 201,
|
||||||
|
statusText: 'Created',
|
||||||
|
responseBody: '{"id": "123", "name": "John"}',
|
||||||
|
responseHeaders: [{ key: 'location', value: '/users/123' }],
|
||||||
|
headers: [{ key: 'content-type', value: 'application/json' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.method).toBe('POST');
|
||||||
|
expect(result.statusCode).toBe(201);
|
||||||
|
expect(result.requestHeaders).toHaveLength(1);
|
||||||
|
expect(result.responseHeaders).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null on parsing error', () => {
|
||||||
|
// Create an object that will cause URL parsing to fail
|
||||||
|
const exampleData = {
|
||||||
|
key: 'bad-example',
|
||||||
|
name: 'Invalid',
|
||||||
|
endpoint: 'http://a bc.com', // This should cause an error
|
||||||
|
responseBody: '{}',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle endpoint with port number', () => {
|
||||||
|
const exampleData = {
|
||||||
|
key: 'example15',
|
||||||
|
name: 'With Port',
|
||||||
|
method: 'GET',
|
||||||
|
endpoint: 'http://api.example.com:8080/users',
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
responseBody: '[]',
|
||||||
|
responseHeaders: [],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.path).toBe('/users');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle different HTTP methods', () => {
|
||||||
|
const methods = [
|
||||||
|
'GET',
|
||||||
|
'POST',
|
||||||
|
'PUT',
|
||||||
|
'PATCH',
|
||||||
|
'DELETE',
|
||||||
|
'HEAD',
|
||||||
|
'OPTIONS',
|
||||||
|
];
|
||||||
|
|
||||||
|
methods.forEach((method) => {
|
||||||
|
const exampleData = {
|
||||||
|
key: `example-${method}`,
|
||||||
|
name: `Test ${method}`,
|
||||||
|
method: method,
|
||||||
|
endpoint: 'http://api.example.com/test',
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
responseBody: '{}',
|
||||||
|
responseHeaders: [],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](
|
||||||
|
exampleData,
|
||||||
|
requestId,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.method).toBe(method);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should preserve endpoint in result', () => {
|
||||||
|
const endpoint = 'http://api.example.com/users/<<id>>?page=1';
|
||||||
|
const exampleData = {
|
||||||
|
key: 'example16',
|
||||||
|
name: 'Preserve Endpoint',
|
||||||
|
method: 'GET',
|
||||||
|
endpoint: endpoint,
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
responseBody: '{}',
|
||||||
|
responseHeaders: [],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.endpoint).toBe(endpoint);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle empty query string', () => {
|
||||||
|
const exampleData = {
|
||||||
|
key: 'example17',
|
||||||
|
name: 'Empty Query',
|
||||||
|
method: 'GET',
|
||||||
|
endpoint: 'http://api.example.com/users?',
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
responseBody: '[]',
|
||||||
|
responseHeaders: [],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mockServerService['parseExample'](exampleData, requestId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.path).toBe('/users');
|
||||||
|
expect(result.queryParams).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1016,23 +1016,38 @@ export class MockServerService {
|
||||||
private parseExample(exampleData: any, requestId: string) {
|
private parseExample(exampleData: any, requestId: string) {
|
||||||
try {
|
try {
|
||||||
// Parse endpoint to extract path and query parameters
|
// Parse endpoint to extract path and query parameters
|
||||||
|
let endpointString = String(exampleData.endpoint ?? '');
|
||||||
let path = '/';
|
let path = '/';
|
||||||
const queryParams: Record<string, string> = {};
|
const queryParams: Record<string, string> = {};
|
||||||
|
|
||||||
if (exampleData.endpoint) {
|
// If endpoint starts with '<<', then cut the string after '>>'
|
||||||
const url = new URL(
|
if (endpointString.startsWith('<<')) {
|
||||||
exampleData.endpoint,
|
const endIndex = endpointString.indexOf('>>');
|
||||||
'http://dummy.com', // Base URL for parsing
|
if (endIndex !== -1) {
|
||||||
);
|
endpointString = endpointString.slice(endIndex + 2);
|
||||||
// Decode the pathname to preserve Hoppscotch variable syntax (<<variable>>)
|
}
|
||||||
path = decodeURIComponent(url.pathname);
|
|
||||||
|
|
||||||
// Extract query parameters
|
|
||||||
url.searchParams.forEach((value, key) => {
|
|
||||||
queryParams[key] = value;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove domain if present
|
||||||
|
endpointString = endpointString.replace(
|
||||||
|
/^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}/,
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use URL to parse path and query parameters
|
||||||
|
const url = new URL(
|
||||||
|
endpointString,
|
||||||
|
'http://dummy.com', // Base URL for parsing
|
||||||
|
);
|
||||||
|
|
||||||
|
// Decode the pathname to preserve Hoppscotch variable syntax (<<variable>>)
|
||||||
|
path = decodeURIComponent(url.pathname);
|
||||||
|
|
||||||
|
// Extract query parameters
|
||||||
|
url.searchParams.forEach((value, key) => {
|
||||||
|
queryParams[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: exampleData.key || `${requestId}-${exampleData.name}`,
|
id: exampleData.key || `${requestId}-${exampleData.name}`,
|
||||||
name: exampleData.name,
|
name: exampleData.name,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue