Skip to content

Commit 20faaba

Browse files
authored
refactor(server): pf-3836 processDocsFunction, metadata, errors (#209)
* getResources, refactor processDocsFunction for metadata, errors * helpers, new createError, isUrlObject, parseUrl
1 parent fea5c19 commit 20faaba

5 files changed

Lines changed: 513 additions & 61 deletions

File tree

src/__tests__/__snapshots__/server.getResources.test.ts.snap

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,19 @@ exports[`processDocsFunction should handle errors gracefully: errors 1`] = `
1212
"content": "❌ Failed to load bad-file.md: File not found",
1313
"isSuccess": false,
1414
"path": "bad-file.md",
15-
"resolvedPath": undefined,
15+
"resolvedPath": "/bad-file.md",
16+
},
17+
]
18+
`;
19+
20+
exports[`processDocsFunction should process local and remote inputs, de-duplicate with first metadata 1`] = `
21+
[
22+
{
23+
"content": "local file content",
24+
"isSuccess": true,
25+
"lorem": "ipsum",
26+
"path": "file.md",
27+
"resolvedPath": "/file.md",
1628
},
1729
]
1830
`;
@@ -51,7 +63,7 @@ exports[`processDocsFunction should process local and remote inputs, files and U
5163
]
5264
`;
5365

54-
exports[`processDocsFunction should process local and remote inputs, filter empty strings 1`] = `
66+
exports[`processDocsFunction should process local and remote inputs, filter empty strings with varied input 1`] = `
5567
[
5668
{
5769
"content": "local file content",
@@ -68,23 +80,38 @@ exports[`processDocsFunction should process local and remote inputs, filter empt
6880
]
6981
`;
7082

83+
exports[`processDocsFunction should process local and remote inputs, metadata passthrough 1`] = `
84+
[
85+
{
86+
"content": "local file content",
87+
"dolor": "sit",
88+
"isSuccess": true,
89+
"lorem": "ispum",
90+
"path": "file.md",
91+
"resolvedPath": "/file.md",
92+
},
93+
]
94+
`;
95+
7196
exports[`promiseQueue should execute promises in order: allSettled 1`] = `
7297
[
7398
{
7499
"status": "fulfilled",
75100
"value": {
76101
"content": "/dolor-sit.md",
102+
"path": "dolor-sit.md",
77103
"resolvedPath": "/dolor-sit.md",
78104
},
79105
},
80106
{
81-
"reason": "https://example.com/remote.md",
107+
"reason": [Error: https://example.com/remote.md],
82108
"status": "rejected",
83109
},
84110
{
85111
"status": "fulfilled",
86112
"value": {
87113
"content": "/lorem-ipsum.md",
114+
"path": "lorem-ipsum.md",
88115
"resolvedPath": "/lorem-ipsum.md",
89116
},
90117
},

src/__tests__/server.getResources.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ describe('loadFileFetch', () => {
320320
expect(mockReadCall).toHaveBeenCalledTimes(expectedIsFetch ? 0 : 1);
321321
expect(result).toEqual({
322322
content: 'content',
323+
path: expect.any(String),
323324
resolvedPath: expect.any(String)
324325
});
325326
});
@@ -374,15 +375,33 @@ describe('processDocsFunction', () => {
374375
fetchMemoHits: 1
375376
},
376377
{
377-
description: 'filter empty strings',
378+
description: 'filter empty strings with varied input',
378379
inputs: [
380+
{ doc: 'file.md' },
379381
'file.md',
380382
'',
381383
' ',
382384
'file2.md'
383385
],
384386
options: {},
385387
fileMemoHits: 2
388+
},
389+
{
390+
description: 'de-duplicate with first metadata',
391+
inputs: [
392+
{ doc: 'file.md', lorem: 'ipsum' },
393+
{ doc: 'file.md', dolor: 'sit' }
394+
],
395+
options: {},
396+
fileMemoHits: 1
397+
},
398+
{
399+
description: 'metadata passthrough',
400+
inputs: [
401+
{ doc: 'file.md', lorem: 'ispum', dolor: 'sit' }
402+
],
403+
options: {},
404+
fileMemoHits: 1
386405
}
387406
])('should process local and remote inputs, $description', async ({ inputs, options, fileMemoHits = 0, fetchMemoHits = 0 }) => {
388407
const result = await processDocsFunction(inputs, {

src/__tests__/server.helpers.test.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
buildSearchString,
3+
createError,
34
freezeObject,
45
generateHash,
56
hashCode,
@@ -8,11 +9,13 @@ import {
89
isPromise,
910
isReferenceLike,
1011
isUrl,
12+
isUrlObject,
1113
isPath,
1214
isWhitelistedUrl,
1315
listAllCombinations,
1416
listIncrementalCombinations,
1517
mergeObjects,
18+
parseUrl,
1619
portValid,
1720
splitUri,
1821
stringJoin,
@@ -57,12 +60,66 @@ describe('buildSearchString', () => {
5760
lorem: 'ipsum dolor'
5861
},
5962
expected: 'lorem=ipsum+dolor'
63+
},
64+
{
65+
description: 'prefix with base that has query',
66+
values: {
67+
lorem: 'ipsum'
68+
},
69+
options: {
70+
prefix: true,
71+
base: 'patternfly://docs?version=1'
72+
},
73+
expected: '&lorem=ipsum'
6074
}
6175
])('should build a search string, $description', ({ values, options, expected }) => {
6276
expect(buildSearchString(values, options || {})).toBe(expected);
6377
});
6478
});
6579

80+
describe('createError', () => {
81+
it.each([
82+
{
83+
description: 'use an explicit message and metadata',
84+
message: 'Custom error message',
85+
metadata: { code: 404, details: 'Not found' },
86+
expectedMessage: 'Custom error message',
87+
expectedMetadata: { code: 404, details: 'Not found' }
88+
},
89+
{
90+
description: 'use an Error object as message',
91+
message: new Error('Original error message'),
92+
metadata: { path: '/tmp' },
93+
expectedMessage: 'Original error message',
94+
expectedMetadata: { path: '/tmp' }
95+
},
96+
{
97+
description: 'use cause message when message is undefined',
98+
message: undefined,
99+
options: { cause: new Error('Cause error') },
100+
metadata: { retry: true },
101+
expectedMessage: 'Cause error',
102+
expectedMetadata: { retry: true }
103+
},
104+
{
105+
description: 'use fallback message when nothing else is provided',
106+
message: undefined,
107+
metadata: {},
108+
expectedMessage: 'An error occurred',
109+
expectedMetadata: {}
110+
}
111+
])('should create an error, $description', ({ message, options, metadata, expectedMessage, expectedMetadata }: any) => {
112+
const err = createError(message, options || {}, metadata);
113+
114+
expect(err).toBeInstanceOf(Error);
115+
expect(err.message).toBe(expectedMessage);
116+
Object.entries(expectedMetadata).forEach(([key, value]) => {
117+
expect((err as any)[key]).toBe(value);
118+
});
119+
expect(err.cause).toBe(options?.cause);
120+
});
121+
});
122+
66123
describe('freezeObject', () => {
67124
it.each([
68125
{
@@ -463,6 +520,55 @@ describe('isPromise', () => {
463520
});
464521
});
465522

523+
describe('isUrlObject', () => {
524+
it.each([
525+
{
526+
description: 'valid URL object with no protocol restrictions',
527+
obj: new URL('https://patternfly.org'),
528+
options: {},
529+
expected: true
530+
},
531+
{
532+
description: 'valid URL object with matching allowed protocol',
533+
obj: new URL('patternfly://docs'),
534+
options: { allowedProtocols: ['patternfly'] },
535+
expected: true
536+
},
537+
{
538+
description: 'valid URL object with non-matching allowed protocol',
539+
obj: new URL('http://patternfly.org'),
540+
options: { allowedProtocols: ['https'] },
541+
expected: false
542+
},
543+
{
544+
description: 'invalid input (string instead of URL object)',
545+
obj: 'https://patternfly.org',
546+
options: {},
547+
expected: false
548+
},
549+
{
550+
description: 'invalid input, null',
551+
obj: null,
552+
options: {},
553+
expected: false
554+
},
555+
{
556+
description: 'invalid input, undefined',
557+
obj: undefined,
558+
options: {},
559+
expected: false
560+
},
561+
{
562+
description: 'invalid input, empty string',
563+
obj: ' ',
564+
options: {},
565+
expected: false
566+
}
567+
])('should return $expected for $description', ({ obj, options, expected }) => {
568+
expect(isUrlObject(obj, options)).toBe(expected);
569+
});
570+
});
571+
466572
describe('isPlainObject', () => {
467573
it.each([
468574
{
@@ -729,6 +835,11 @@ describe('isWhitelistedUrl', () => {
729835
description: 'invalid URL string',
730836
url: 'not-a-url',
731837
expected: false
838+
},
839+
{
840+
description: 'reject encoded slash in pathname',
841+
url: 'https://github.com/patternfly%2Fother-repo',
842+
expected: false
732843
}
733844
])('should confirm if a URL is whitelisted, $description', ({ url, expected }) => {
734845
const whitelist = [
@@ -950,6 +1061,84 @@ describe('portValid', () => {
9501061
});
9511062
});
9521063

1064+
describe('parseUrl', () => {
1065+
it.each([
1066+
{
1067+
description: 'absolute URI without prefix',
1068+
uri: 'patternfly://docs/button?v=1',
1069+
options: { isStrict: false },
1070+
expected: {
1071+
protocol: 'patternfly:',
1072+
hostname: 'docs',
1073+
path: 'button',
1074+
params: { v: '1' }
1075+
}
1076+
},
1077+
{
1078+
description: 'relative URI with prefix',
1079+
uri: 'docs/button',
1080+
options: { prefix: 'patternfly', isStrict: false },
1081+
expected: {
1082+
protocol: 'patternfly:',
1083+
hostname: 'docs',
1084+
path: 'button',
1085+
params: {}
1086+
}
1087+
},
1088+
{
1089+
description: 'absolute URI with ignored prefix',
1090+
uri: 'https://google.com/search?q=test',
1091+
options: { prefix: 'patternfly://' },
1092+
expected: {
1093+
protocol: 'https:',
1094+
hostname: 'google.com',
1095+
path: 'search',
1096+
params: { q: 'test' }
1097+
}
1098+
},
1099+
{
1100+
description: 'invalid URI',
1101+
uri: 'not a uri',
1102+
expected: undefined
1103+
},
1104+
{
1105+
description: 'relative URI without prefix',
1106+
uri: 'docs/button',
1107+
expected: undefined
1108+
},
1109+
{
1110+
description: 'absolute URI without hostname',
1111+
uri: 'patternfly:docs/button',
1112+
options: { isStrict: false },
1113+
expected: {
1114+
protocol: 'patternfly:',
1115+
hostname: '',
1116+
path: 'docs/button',
1117+
params: {}
1118+
}
1119+
},
1120+
{
1121+
description: 'pass through a malformed percent-encoding in absolute URI',
1122+
uri: 'https://patternfly.org/docs/bad%zzpath',
1123+
options: {},
1124+
expected: {
1125+
protocol: 'https:',
1126+
hostname: 'patternfly.org',
1127+
path: 'docs/bad%zzpath',
1128+
params: {}
1129+
}
1130+
},
1131+
{
1132+
description: 'return undefined for malformed percent-encoding in prefixed path',
1133+
uri: 'docs/bad%zzpath',
1134+
options: { prefix: 'patternfly' },
1135+
expected: undefined
1136+
}
1137+
])('should parse a URI, $description', ({ uri, options, expected }) => {
1138+
expect(parseUrl(uri, options)).toEqual(expected);
1139+
});
1140+
});
1141+
9531142
describe('splitUri', () => {
9541143
it.each([
9551144
{

0 commit comments

Comments
 (0)