@elisra-devops/docgen-data-provider 1.140.0 → 1.142.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +2 -0
- package/bin/helpers/requestContext.d.ts +51 -0
- package/bin/helpers/requestContext.js +218 -0
- package/bin/helpers/requestContext.js.map +1 -0
- package/bin/helpers/tfs.d.ts +5 -0
- package/bin/helpers/tfs.js +24 -24
- package/bin/helpers/tfs.js.map +1 -1
- package/bin/modules/GitDataProvider.js +3 -2
- package/bin/modules/GitDataProvider.js.map +1 -1
- package/bin/modules/JfrogDataProvider.js +3 -2
- package/bin/modules/JfrogDataProvider.js.map +1 -1
- package/bin/modules/PipelinesDataProvider.js +5 -4
- package/bin/modules/PipelinesDataProvider.js.map +1 -1
- package/bin/modules/ResultDataProvider.js +32 -25
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/modules/TestDataProvider.js +4 -3
- package/bin/modules/TestDataProvider.js.map +1 -1
- package/bin/modules/TicketsDataProvider.js +26 -22
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/helpers/requestContext.test.d.ts +1 -0
- package/bin/tests/helpers/requestContext.test.js +165 -0
- package/bin/tests/helpers/requestContext.test.js.map +1 -0
- package/bin/tests/helpers/tfs.test.js +54 -0
- package/bin/tests/helpers/tfs.test.js.map +1 -1
- package/bin/tests/modules/JfrogDataProvider.test.js +4 -2
- package/bin/tests/modules/JfrogDataProvider.test.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +9 -3
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/bin/tests/modules/gitDataProvider.test.js +1 -1
- package/bin/tests/modules/gitDataProvider.test.js.map +1 -1
- package/bin/tests/modules/pipelineDataProvider.test.js +1 -1
- package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -1
- package/bin/tests/modules/testDataProvider.test.js +1 -1
- package/bin/tests/modules/testDataProvider.test.js.map +1 -1
- package/bin/tests/modules/ticketsDataProvider.test.js +2 -2
- package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -1
- package/bin/tests/utils/logger.test.d.ts +1 -0
- package/bin/tests/utils/logger.test.js +332 -0
- package/bin/tests/utils/logger.test.js.map +1 -0
- package/bin/tests/utils/runContext.test.d.ts +1 -0
- package/bin/tests/utils/runContext.test.js +51 -0
- package/bin/tests/utils/runContext.test.js.map +1 -0
- package/bin/utils/logSink.d.ts +39 -0
- package/bin/utils/logSink.js +28 -0
- package/bin/utils/logSink.js.map +1 -0
- package/bin/utils/logger.d.ts +7 -0
- package/bin/utils/logger.js +266 -27
- package/bin/utils/logger.js.map +1 -1
- package/bin/utils/runContext.d.ts +8 -0
- package/bin/utils/runContext.js +12 -0
- package/bin/utils/runContext.js.map +1 -0
- package/eslint.config.mjs +40 -0
- package/package.json +8 -3
- package/src/helpers/requestContext.ts +222 -0
- package/src/helpers/tfs.ts +30 -24
- package/src/modules/GitDataProvider.ts +3 -2
- package/src/modules/JfrogDataProvider.ts +3 -2
- package/src/modules/PipelinesDataProvider.ts +5 -4
- package/src/modules/ResultDataProvider.ts +31 -25
- package/src/modules/TestDataProvider.ts +4 -3
- package/src/modules/TicketsDataProvider.ts +28 -26
- package/src/tests/helpers/requestContext.test.ts +202 -0
- package/src/tests/helpers/tfs.test.ts +86 -0
- package/src/tests/modules/JfrogDataProvider.test.ts +5 -2
- package/src/tests/modules/ResultDataProvider.test.ts +10 -3
- package/src/tests/modules/gitDataProvider.test.ts +2 -1
- package/src/tests/modules/pipelineDataProvider.test.ts +2 -1
- package/src/tests/modules/testDataProvider.test.ts +4 -1
- package/src/tests/modules/ticketsDataProvider.test.ts +2 -2
- package/src/tests/utils/logger.test.ts +381 -0
- package/src/tests/utils/runContext.test.ts +48 -0
- package/src/utils/logSink.ts +59 -0
- package/src/utils/logger.ts +269 -29
- package/src/utils/runContext.ts +25 -0
|
@@ -10,6 +10,7 @@ import { TestSteps } from '../models/tfs-data';
|
|
|
10
10
|
import TestStepParserHelper from '../utils/testStepParserHelper';
|
|
11
11
|
|
|
12
12
|
import logger from '../utils/logger';
|
|
13
|
+
import { describeError } from '../helpers/requestContext';
|
|
13
14
|
const pLimit = require('p-limit');
|
|
14
15
|
|
|
15
16
|
type FallbackFetchOutcome = {
|
|
@@ -491,7 +492,7 @@ export default class TicketsDataProvider {
|
|
|
491
492
|
break;
|
|
492
493
|
}
|
|
493
494
|
} catch (err: any) {
|
|
494
|
-
logger.error(
|
|
495
|
+
logger.error('Error occurred during fetching shared queries:', describeError(err));
|
|
495
496
|
throw err;
|
|
496
497
|
}
|
|
497
498
|
}
|
|
@@ -527,7 +528,7 @@ export default class TicketsDataProvider {
|
|
|
527
528
|
})
|
|
528
529
|
);
|
|
529
530
|
} catch (err: any) {
|
|
530
|
-
logger.error(
|
|
531
|
+
logger.error('Error occurred during fetching fields by type:', describeError(err));
|
|
531
532
|
throw err;
|
|
532
533
|
}
|
|
533
534
|
}
|
|
@@ -558,7 +559,7 @@ export default class TicketsDataProvider {
|
|
|
558
559
|
}
|
|
559
560
|
return { types, states: Array.from(stateSet) };
|
|
560
561
|
} catch (err: any) {
|
|
561
|
-
logger.error(
|
|
562
|
+
logger.error('Error fetching work item type states:', describeError(err));
|
|
562
563
|
throw err;
|
|
563
564
|
}
|
|
564
565
|
}
|
|
@@ -783,11 +784,9 @@ export default class TicketsDataProvider {
|
|
|
783
784
|
: null;
|
|
784
785
|
} catch (err: any) {
|
|
785
786
|
logger.error(
|
|
786
|
-
`Error occurred while constructing the customer query list
|
|
787
|
-
|
|
788
|
-
)}`,
|
|
787
|
+
`Error occurred while constructing the customer query list with query ${JSON.stringify(rootQuery)}`,
|
|
788
|
+
err,
|
|
789
789
|
);
|
|
790
|
-
logger.error(`Error stack ${err.message}`);
|
|
791
790
|
return null;
|
|
792
791
|
}
|
|
793
792
|
}
|
|
@@ -1288,7 +1287,7 @@ export default class TicketsDataProvider {
|
|
|
1288
1287
|
break;
|
|
1289
1288
|
}
|
|
1290
1289
|
} catch (err: any) {
|
|
1291
|
-
logger.error(`Could not fetch query results for ${wiqlHref}
|
|
1290
|
+
logger.error(`Could not fetch query results for ${wiqlHref}:`, describeError(err));
|
|
1292
1291
|
}
|
|
1293
1292
|
}
|
|
1294
1293
|
|
|
@@ -1664,7 +1663,7 @@ export default class TicketsDataProvider {
|
|
|
1664
1663
|
}
|
|
1665
1664
|
return [...workItemsResultMap.values()];
|
|
1666
1665
|
} catch (error: any) {
|
|
1667
|
-
logger.error(
|
|
1666
|
+
logger.error('could not parse requested flat query:', describeError(error));
|
|
1668
1667
|
}
|
|
1669
1668
|
}
|
|
1670
1669
|
|
|
@@ -2697,8 +2696,10 @@ export default class TicketsDataProvider {
|
|
|
2697
2696
|
const subRes = await postBatch(currentIds);
|
|
2698
2697
|
res.push(...subRes.value);
|
|
2699
2698
|
} catch (error) {
|
|
2700
|
-
|
|
2701
|
-
|
|
2699
|
+
// JSON.stringify(error) on an AxiosError calls its own .toJSON(), which serializes
|
|
2700
|
+
// config.auth.password / config.headers.Authorization — the PAT, in plaintext. Pass
|
|
2701
|
+
// the error as meta instead and let the logger's redact format scrub it.
|
|
2702
|
+
logger.error(`error populating workitems array`, error);
|
|
2702
2703
|
return [];
|
|
2703
2704
|
}
|
|
2704
2705
|
}
|
|
@@ -2708,8 +2709,10 @@ export default class TicketsDataProvider {
|
|
|
2708
2709
|
const subRes = await postBatch(currentIds);
|
|
2709
2710
|
res.push(...subRes.value);
|
|
2710
2711
|
} catch (error) {
|
|
2711
|
-
|
|
2712
|
-
|
|
2712
|
+
// JSON.stringify(error) on an AxiosError calls its own .toJSON(), which serializes
|
|
2713
|
+
// config.auth.password / config.headers.Authorization — the PAT, in plaintext. Pass
|
|
2714
|
+
// the error as meta instead and let the logger's redact format scrub it.
|
|
2715
|
+
logger.error(`error populating workitems array`, error);
|
|
2713
2716
|
return [];
|
|
2714
2717
|
}
|
|
2715
2718
|
}
|
|
@@ -2843,8 +2846,9 @@ export default class TicketsDataProvider {
|
|
|
2843
2846
|
);
|
|
2844
2847
|
return attachmentList;
|
|
2845
2848
|
} catch (e) {
|
|
2846
|
-
|
|
2847
|
-
|
|
2849
|
+
// JSON.stringify(e) on an Error returns "{}" — own enumerable props only, message/stack
|
|
2850
|
+
// are non-enumerable — so this logged literally nothing useful. Pass it as meta instead.
|
|
2851
|
+
logger.error(`error fetching attachments for work item ${id}`, e);
|
|
2848
2852
|
return [];
|
|
2849
2853
|
}
|
|
2850
2854
|
}
|
|
@@ -3161,11 +3165,9 @@ export default class TicketsDataProvider {
|
|
|
3161
3165
|
return { tree1: tree1Node, tree2: tree2Node };
|
|
3162
3166
|
} catch (err: any) {
|
|
3163
3167
|
logger.error(
|
|
3164
|
-
`Error occurred while constructing the query list
|
|
3165
|
-
|
|
3166
|
-
)}`,
|
|
3168
|
+
`Error occurred while constructing the query list with query ${JSON.stringify(rootQuery)}`,
|
|
3169
|
+
err,
|
|
3167
3170
|
);
|
|
3168
|
-
logger.error(`Error stack ${err.message}`);
|
|
3169
3171
|
}
|
|
3170
3172
|
}
|
|
3171
3173
|
|
|
@@ -3561,7 +3563,7 @@ export default class TicketsDataProvider {
|
|
|
3561
3563
|
|
|
3562
3564
|
item.fields = { ...parsedFields };
|
|
3563
3565
|
} catch (err: any) {
|
|
3564
|
-
logger.error(
|
|
3566
|
+
logger.error('Cannot filter columns:', describeError(err));
|
|
3565
3567
|
throw err;
|
|
3566
3568
|
}
|
|
3567
3569
|
}
|
|
@@ -3616,7 +3618,7 @@ export default class TicketsDataProvider {
|
|
|
3616
3618
|
|
|
3617
3619
|
return workItemTypesWithIcons;
|
|
3618
3620
|
} catch (err: any) {
|
|
3619
|
-
logger.error(
|
|
3621
|
+
logger.error('Error occurred during fetching work item types:', describeError(err));
|
|
3620
3622
|
throw err;
|
|
3621
3623
|
}
|
|
3622
3624
|
}
|
|
@@ -3769,7 +3771,7 @@ export default class TicketsDataProvider {
|
|
|
3769
3771
|
categorizedRequirements['Precedence and Criticality of Requirements'].push(requirementItem);
|
|
3770
3772
|
}
|
|
3771
3773
|
} catch (err: any) {
|
|
3772
|
-
logger.warn(`Could not fetch work item ${workItemId}
|
|
3774
|
+
logger.warn(`Could not fetch work item ${workItemId}:`, describeError(err));
|
|
3773
3775
|
}
|
|
3774
3776
|
}
|
|
3775
3777
|
|
|
@@ -3791,7 +3793,7 @@ export default class TicketsDataProvider {
|
|
|
3791
3793
|
totalCount: workItemIds.length,
|
|
3792
3794
|
};
|
|
3793
3795
|
} catch (err: any) {
|
|
3794
|
-
logger.error(
|
|
3796
|
+
logger.error('Could not fetch categorized requirements:', describeError(err));
|
|
3795
3797
|
throw err;
|
|
3796
3798
|
}
|
|
3797
3799
|
}
|
|
@@ -3886,7 +3888,7 @@ export default class TicketsDataProvider {
|
|
|
3886
3888
|
const relations = qr?.workItemRelations || [];
|
|
3887
3889
|
result['req-test'] = await resolveQuerySides(cols, relations, true);
|
|
3888
3890
|
} catch (err: any) {
|
|
3889
|
-
logger.error(
|
|
3891
|
+
logger.error('GetTraceColumnsByType[req-test] failed:', describeError(err));
|
|
3890
3892
|
result['req-test'] = { Requirement: [], 'Test Case': [] };
|
|
3891
3893
|
}
|
|
3892
3894
|
}
|
|
@@ -3898,14 +3900,14 @@ export default class TicketsDataProvider {
|
|
|
3898
3900
|
const relations = qr?.workItemRelations || [];
|
|
3899
3901
|
result['test-req'] = await resolveQuerySides(cols, relations, false);
|
|
3900
3902
|
} catch (err: any) {
|
|
3901
|
-
logger.error(
|
|
3903
|
+
logger.error('GetTraceColumnsByType[test-req] failed:', describeError(err));
|
|
3902
3904
|
result['test-req'] = { Requirement: [], 'Test Case': [] };
|
|
3903
3905
|
}
|
|
3904
3906
|
}
|
|
3905
3907
|
|
|
3906
3908
|
return result;
|
|
3907
3909
|
} catch (err: any) {
|
|
3908
|
-
logger.error(
|
|
3910
|
+
logger.error('GetTraceColumnsByType failed:', describeError(err));
|
|
3909
3911
|
return {};
|
|
3910
3912
|
}
|
|
3911
3913
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sanitizeUrl,
|
|
3
|
+
summarizeBody,
|
|
4
|
+
summarizeResponse,
|
|
5
|
+
buildAdoRequestContext,
|
|
6
|
+
annotateAdoError,
|
|
7
|
+
maskWiqlLiterals,
|
|
8
|
+
} from '../../helpers/requestContext';
|
|
9
|
+
|
|
10
|
+
describe('maskWiqlLiterals', () => {
|
|
11
|
+
it('replaces every quoted literal with its length', () => {
|
|
12
|
+
expect(maskWiqlLiterals("a = 'xy' and b = ''")).toBe("a = '[2 chars]' and b = '[0 chars]'");
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('sanitizeUrl', () => {
|
|
17
|
+
it('returns undefined for a missing or empty url', () => {
|
|
18
|
+
expect(sanitizeUrl(undefined)).toBeUndefined();
|
|
19
|
+
expect(sanitizeUrl('')).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('keeps a normal ADO url intact', () => {
|
|
23
|
+
const url = 'https://dev.azure.com/org/proj/_apis/wit/queries/abc?$depth=1&api-version=7.0';
|
|
24
|
+
expect(sanitizeUrl(url)).toBe(url);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('strips credentials embedded in the url', () => {
|
|
28
|
+
const out = sanitizeUrl('https://user:s3cret@dev.azure.com/org/_apis/projects');
|
|
29
|
+
expect(out).not.toContain('s3cret');
|
|
30
|
+
expect(out).not.toContain('user');
|
|
31
|
+
expect(out).toContain('dev.azure.com/org/_apis/projects');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('masks sensitive query parameter values but keeps ordinary ones', () => {
|
|
35
|
+
const out = sanitizeUrl('https://host/api?access_token=abc123&top=10&sig=zzz&keywords=release')!;
|
|
36
|
+
expect(out).not.toContain('abc123');
|
|
37
|
+
expect(out).not.toContain('zzz');
|
|
38
|
+
expect(out).toContain('top=10');
|
|
39
|
+
expect(out).toContain('keywords=release');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('leaves non-sensitive parameters byte-identical when masking another', () => {
|
|
43
|
+
expect(sanitizeUrl('https://h/api?$filter=a%20b&token=x&q=a+b')).toBe('https://h/api?$filter=a%20b&token=[REDACTED]&q=a+b');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('passes a non-absolute url through, bounded', () => {
|
|
47
|
+
expect(sanitizeUrl('/relative/path')).toBe('/relative/path');
|
|
48
|
+
expect(sanitizeUrl('x'.repeat(5000))!.length).toBeLessThanOrEqual(1001);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('summarizeBody', () => {
|
|
53
|
+
it('never describes a GET body', () => {
|
|
54
|
+
expect(summarizeBody({ query: 'SELECT 1' }, 'get')).toBeUndefined();
|
|
55
|
+
expect(summarizeBody({ query: 'SELECT 1' }, 'HEAD')).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns undefined for an empty body — the default `{}` callers pass', () => {
|
|
59
|
+
expect(summarizeBody({}, 'post')).toBeUndefined();
|
|
60
|
+
expect(summarizeBody([], 'post')).toBeUndefined();
|
|
61
|
+
expect(summarizeBody(null, 'post')).toBeUndefined();
|
|
62
|
+
expect(summarizeBody('', 'post')).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('keeps a WIQL-style body so the failing query is visible', () => {
|
|
66
|
+
const out = summarizeBody({ query: "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = 'P'" }, 'post')!;
|
|
67
|
+
expect(out).toContain('SELECT [System.Id] FROM WorkItems');
|
|
68
|
+
expect(out).toContain("[System.TeamProject] = '[1 chars]'");
|
|
69
|
+
expect(out).not.toContain("= 'P'");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('masks WIQL literals (incl. escaped quotes) but keeps fields, numbers and macros', () => {
|
|
73
|
+
const out = summarizeBody({ query: "SELECT [Id] FROM WorkItems WHERE [Title] = 'it''s secret' AND [Assigned] = @Me AND [Id] > 42" }, 'post')!;
|
|
74
|
+
expect(out).not.toContain('secret');
|
|
75
|
+
expect(out).toContain("[Title] = '[12 chars]'");
|
|
76
|
+
expect(out).toContain('@Me');
|
|
77
|
+
expect(out).toContain('> 42');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('keeps an id-list body', () => {
|
|
81
|
+
expect(summarizeBody({ ids: [1, 2, 3], fields: ['System.Title'] }, 'post')).toBe(
|
|
82
|
+
'{"ids":[1,2,3],"fields":["System.Title"]}'
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('redacts credential-looking keys at any depth', () => {
|
|
87
|
+
const out = summarizeBody({ query: 'q', auth: { token: 'tok-123', password: 'pw-456' }, pat: 'pat-789' }, 'post')!;
|
|
88
|
+
expect(out).not.toContain('tok-123');
|
|
89
|
+
expect(out).not.toContain('pw-456');
|
|
90
|
+
expect(out).not.toContain('pat-789');
|
|
91
|
+
expect(out).toContain('[REDACTED]');
|
|
92
|
+
expect(out).toContain('"query":"q"');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('JSON-Patch bodies keep op + path only — field values are dropped', () => {
|
|
96
|
+
const patch = [
|
|
97
|
+
{ op: 'add', path: '/fields/System.Title', value: 'Highly classified title' },
|
|
98
|
+
{ op: 'replace', path: '/fields/System.Description', value: { html: '<p>secret</p>' } },
|
|
99
|
+
];
|
|
100
|
+
const out = summarizeBody(patch, 'patch')!;
|
|
101
|
+
expect(out).toContain('json-patch (2 ops, values omitted)');
|
|
102
|
+
expect(out).toContain('add /fields/System.Title');
|
|
103
|
+
expect(out).toContain('replace /fields/System.Description');
|
|
104
|
+
expect(out).not.toContain('classified');
|
|
105
|
+
expect(out).not.toContain('secret');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('bounds a long string value and the whole body', () => {
|
|
109
|
+
const one = summarizeBody({ query: 'x'.repeat(5000) }, 'post')!;
|
|
110
|
+
expect(one.length).toBeLessThan(700);
|
|
111
|
+
const many = summarizeBody(Object.fromEntries(Array.from({ length: 300 }, (_, i) => [`k${i}`, 'v'.repeat(40)])), 'post')!;
|
|
112
|
+
expect(many.length).toBeLessThanOrEqual(2001);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('caps a very long array', () => {
|
|
116
|
+
const out = summarizeBody({ ids: Array.from({ length: 500 }, (_, i) => i) }, 'post')!;
|
|
117
|
+
expect(out).toContain('more');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('describes non-JSON bodies by type and size only', () => {
|
|
121
|
+
expect(summarizeBody(Buffer.from('abcdef'), 'post')).toBe('[binary, 6 bytes]');
|
|
122
|
+
expect(summarizeBody('not json at all', 'post')).toBe('[text, 15 chars]');
|
|
123
|
+
expect(summarizeBody(42, 'post')).toBe('[number]');
|
|
124
|
+
expect(summarizeBody({ pipe: () => undefined }, 'post')).toBe('[stream]');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('parses a JSON string body and summarizes it like an object', () => {
|
|
128
|
+
expect(summarizeBody('{"query":"q"}', 'post')).toBe('{"query":"q"}');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('never throws on a body it cannot serialize', () => {
|
|
132
|
+
expect(summarizeBody({ n: BigInt(1) }, 'post')).toBe('[unserializable body]');
|
|
133
|
+
const circular: Record<string, unknown> = { a: 1 };
|
|
134
|
+
circular.self = circular;
|
|
135
|
+
expect(() => summarizeBody(circular, 'post')).not.toThrow();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('summarizeResponse', () => {
|
|
140
|
+
it('prefers the server message (ADO puts its explanation there)', () => {
|
|
141
|
+
expect(summarizeResponse({ message: 'TF401232: Work item 5 does not exist', typeName: 'x' })).toBe(
|
|
142
|
+
'TF401232: Work item 5 does not exist'
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('truncates a text response (e.g. an HTML error page) to 200 chars', () => {
|
|
147
|
+
expect(summarizeResponse('<html>'.repeat(200))!.length).toBeLessThanOrEqual(201);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('falls back to a redacted, bounded JSON dump when there is no message', () => {
|
|
151
|
+
const out = summarizeResponse({ code: 1, token: 'leak-me' })!;
|
|
152
|
+
expect(out).not.toContain('leak-me');
|
|
153
|
+
expect(out).toContain('"code":1');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('does not walk a stream response', () => {
|
|
157
|
+
expect(summarizeResponse({ pipe: () => undefined, socket: { a: 1 } })).toBe('[stream]');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('ignores a missing or binary response', () => {
|
|
161
|
+
expect(summarizeResponse(undefined)).toBeUndefined();
|
|
162
|
+
expect(summarizeResponse(Buffer.from('x'))).toBeUndefined();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('buildAdoRequestContext', () => {
|
|
167
|
+
it('describes a failed GET with method, status, attempt and the server message', () => {
|
|
168
|
+
const err = { response: { status: 404, data: { message: 'not found' } } };
|
|
169
|
+
expect(buildAdoRequestContext(err, { method: 'get', url: 'https://h/_apis/x' }, 2)).toEqual({
|
|
170
|
+
method: 'GET',
|
|
171
|
+
url: 'https://h/_apis/x',
|
|
172
|
+
status: 404,
|
|
173
|
+
attempt: 2,
|
|
174
|
+
responseExcerpt: 'not found',
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('includes a summarized body for a POST and omits fields it does not have', () => {
|
|
179
|
+
const ctx = buildAdoRequestContext(new Error('boom'), { method: 'post', url: 'https://h/wiql', data: { query: 'q' } });
|
|
180
|
+
expect(ctx).toEqual({ method: 'POST', url: 'https://h/wiql', requestBody: '{"query":"q"}' });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('falls back to the axios error config when the request is not described explicitly', () => {
|
|
184
|
+
const ctx = buildAdoRequestContext({ config: { method: 'put', url: 'https://h/y' } }, {});
|
|
185
|
+
expect(ctx.method).toBe('PUT');
|
|
186
|
+
expect(ctx.url).toBe('https://h/y');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('annotateAdoError', () => {
|
|
191
|
+
it('attaches the context as an enumerable property winston will merge onto the record', () => {
|
|
192
|
+
const err = annotateAdoError(new Error('x'), { method: 'GET', url: 'https://h/z' });
|
|
193
|
+
expect(Object.keys(err)).toContain('adoRequest');
|
|
194
|
+
expect((err as any).adoRequest.url).toBe('https://h/z');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('never throws on a frozen or non-object error', () => {
|
|
198
|
+
expect(() => annotateAdoError(Object.freeze(new Error('x')), { url: 'u' })).not.toThrow();
|
|
199
|
+
expect(() => annotateAdoError('a string' as any, { url: 'u' })).not.toThrow();
|
|
200
|
+
expect(() => annotateAdoError(undefined as any, { url: 'u' })).not.toThrow();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -1089,3 +1089,89 @@ describe('TFSServices', () => {
|
|
|
1089
1089
|
});
|
|
1090
1090
|
});
|
|
1091
1091
|
});
|
|
1092
|
+
|
|
1093
|
+
describe('TFSServices request-context annotation', () => {
|
|
1094
|
+
// Math.random is pinned and setTimeout collapsed so a retried failure doesn't slow the suite.
|
|
1095
|
+
beforeEach(() => {
|
|
1096
|
+
jest.clearAllMocks();
|
|
1097
|
+
mockAxiosInstance.request.mockReset();
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
const notFound = () => {
|
|
1101
|
+
const e: any = new Error('Request failed with status code 404');
|
|
1102
|
+
e.response = { status: 404, data: { message: 'TF401232: Work item 5 does not exist' } };
|
|
1103
|
+
return e;
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
it('annotates the thrown error with the request even when printError is false, without logging', async () => {
|
|
1107
|
+
mockAxiosInstance.request.mockRejectedValue(notFound());
|
|
1108
|
+
|
|
1109
|
+
const caught = await TFSServices.getItemContent('https://dev.azure.com/org/_apis/wit/queries/q1', 'pat', 'get', {}, {}, false).catch(
|
|
1110
|
+
(e) => e
|
|
1111
|
+
);
|
|
1112
|
+
|
|
1113
|
+
expect(caught.adoRequest).toEqual({
|
|
1114
|
+
method: 'GET',
|
|
1115
|
+
url: 'https://dev.azure.com/org/_apis/wit/queries/q1',
|
|
1116
|
+
status: 404,
|
|
1117
|
+
attempt: 1,
|
|
1118
|
+
responseExcerpt: 'TF401232: Work item 5 does not exist',
|
|
1119
|
+
});
|
|
1120
|
+
expect(logger.error).not.toHaveBeenCalled();
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
it('with printError, also emits the one "ADO request failed" record carrying the same context', async () => {
|
|
1124
|
+
mockAxiosInstance.request.mockRejectedValue(notFound());
|
|
1125
|
+
|
|
1126
|
+
await TFSServices.getItemContent('https://dev.azure.com/org/_apis/x', 'pat').catch(() => undefined);
|
|
1127
|
+
|
|
1128
|
+
expect(logger.error).toHaveBeenCalledTimes(1);
|
|
1129
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
1130
|
+
'ADO request failed',
|
|
1131
|
+
expect.objectContaining({
|
|
1132
|
+
adoRequest: expect.objectContaining({ method: 'GET', url: 'https://dev.azure.com/org/_apis/x', status: 404 }),
|
|
1133
|
+
})
|
|
1134
|
+
);
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
it('never leaks the PAT: credentials are applied to the axios config, not the description', async () => {
|
|
1138
|
+
mockAxiosInstance.request.mockRejectedValue(notFound());
|
|
1139
|
+
|
|
1140
|
+
const caught = await TFSServices.getItemContent('https://dev.azure.com/org/_apis/x', 'super-secret-pat', 'get', {}, {}, false).catch(
|
|
1141
|
+
(e) => e
|
|
1142
|
+
);
|
|
1143
|
+
|
|
1144
|
+
expect(JSON.stringify(caught.adoRequest)).not.toContain('super-secret-pat');
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
it('postRequest describes a JSON-Patch body by op + path only', async () => {
|
|
1148
|
+
mockAxiosInstance.request.mockRejectedValue(notFound());
|
|
1149
|
+
|
|
1150
|
+
const caught = await TFSServices.postRequest(
|
|
1151
|
+
'https://dev.azure.com/org/_apis/wit/workitems/9',
|
|
1152
|
+
'pat',
|
|
1153
|
+
'PATCH',
|
|
1154
|
+
[{ op: 'add', path: '/fields/System.Title', value: 'confidential title' }]
|
|
1155
|
+
).catch((e) => e);
|
|
1156
|
+
|
|
1157
|
+
expect(caught.adoRequest.method).toBe('PATCH');
|
|
1158
|
+
expect(caught.adoRequest.requestBody).toContain('add /fields/System.Title');
|
|
1159
|
+
expect(caught.adoRequest.requestBody).not.toContain('confidential');
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
it('getJfrogRequest annotates too', async () => {
|
|
1163
|
+
mockAxiosInstance.request.mockRejectedValue(notFound());
|
|
1164
|
+
|
|
1165
|
+
const caught = await TFSServices.getJfrogRequest('https://jfrog.example/api/x').catch((e) => e);
|
|
1166
|
+
|
|
1167
|
+
expect(caught.adoRequest).toEqual(expect.objectContaining({ method: 'GET', url: 'https://jfrog.example/api/x', status: 404 }));
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
it('downloadZipFile annotates the error it rethrows', async () => {
|
|
1171
|
+
mockAxiosInstance.request.mockRejectedValue(notFound());
|
|
1172
|
+
|
|
1173
|
+
const caught = await TFSServices.downloadZipFile('https://dev.azure.com/org/file.zip', 'pat').catch((e) => e);
|
|
1174
|
+
|
|
1175
|
+
expect(caught.adoRequest).toEqual(expect.objectContaining({ method: 'GET', url: 'https://dev.azure.com/org/file.zip', status: 404 }));
|
|
1176
|
+
});
|
|
1177
|
+
});
|
|
@@ -36,7 +36,9 @@ describe('JfrogDataProvider', () => {
|
|
|
36
36
|
`${mockOrgUrl}${mockTeamProject}/_apis/serviceendpoint/endpoints/${mockConnectionId}?api-version=7.1`,
|
|
37
37
|
mockTfsToken
|
|
38
38
|
);
|
|
39
|
-
|
|
39
|
+
// The log used JSON.stringify(url), which wraps a plain string in quotes for no reason —
|
|
40
|
+
// fixed to interpolate the string directly (see docgen-log-redaction / Phase 2).
|
|
41
|
+
expect(logger.debug).toHaveBeenCalledWith(`service connection url ${mockResponse.url}`);
|
|
40
42
|
expect(result).toBe(mockResponse.url);
|
|
41
43
|
});
|
|
42
44
|
|
|
@@ -160,7 +162,8 @@ describe('JfrogDataProvider', () => {
|
|
|
160
162
|
).rejects.toThrow('JFrog API error');
|
|
161
163
|
|
|
162
164
|
expect(logger.error).toHaveBeenCalledWith(
|
|
163
|
-
|
|
165
|
+
'Error occurred during querying JFrog using:',
|
|
166
|
+
expect.objectContaining({ message: 'JFrog API error' })
|
|
164
167
|
);
|
|
165
168
|
});
|
|
166
169
|
});
|
|
@@ -5324,7 +5324,8 @@ describe('ResultDataProvider', () => {
|
|
|
5324
5324
|
);
|
|
5325
5325
|
|
|
5326
5326
|
expect(logger.error).toHaveBeenCalledWith(
|
|
5327
|
-
|
|
5327
|
+
'Could not append related work item to test case 999:',
|
|
5328
|
+
expect.objectContaining({ message: 'network' })
|
|
5328
5329
|
);
|
|
5329
5330
|
});
|
|
5330
5331
|
});
|
|
@@ -6027,8 +6028,14 @@ describe('ResultDataProvider', () => {
|
|
|
6027
6028
|
resultDataProvider.getCombinedResultsSummary(mockTestPlanId, mockProjectName)
|
|
6028
6029
|
).rejects.toThrow('boom');
|
|
6029
6030
|
|
|
6030
|
-
|
|
6031
|
-
|
|
6031
|
+
// Was two separate calls (message, then the response body) — consolidated into one
|
|
6032
|
+
// record with the response summary as meta (docgen-log-redaction / Phase 2/4).
|
|
6033
|
+
expect(logger.error).toHaveBeenCalledWith('Error during getCombinedResultsSummary:', {
|
|
6034
|
+
message: 'boom',
|
|
6035
|
+
stack: expect.any(String),
|
|
6036
|
+
responseSummary: '{"detail":"bad"}',
|
|
6037
|
+
});
|
|
6038
|
+
expect(logger.error).toHaveBeenCalledTimes(1);
|
|
6032
6039
|
});
|
|
6033
6040
|
});
|
|
6034
6041
|
|
|
@@ -367,7 +367,8 @@ describe('GitDataProvider - GetFileFromGitRepo', () => {
|
|
|
367
367
|
|
|
368
368
|
// Assert
|
|
369
369
|
expect(logger.warn).toHaveBeenCalledWith(
|
|
370
|
-
|
|
370
|
+
`File ${mockFileName} could not be read:`,
|
|
371
|
+
expect.objectContaining({ message: mockError.message })
|
|
371
372
|
);
|
|
372
373
|
expect(result).toBeUndefined();
|
|
373
374
|
});
|
|
@@ -305,7 +305,8 @@ describe('PipelinesDataProvider', () => {
|
|
|
305
305
|
null
|
|
306
306
|
);
|
|
307
307
|
expect(logger.error).toHaveBeenCalledWith(
|
|
308
|
-
|
|
308
|
+
'Could not fetch Pipeline Run History:',
|
|
309
|
+
expect.objectContaining({ message: expectedError.message })
|
|
309
310
|
);
|
|
310
311
|
expect(result).toBeUndefined();
|
|
311
312
|
});
|
|
@@ -105,7 +105,10 @@ describe('TestDataProvider', () => {
|
|
|
105
105
|
|
|
106
106
|
// Act & Assert
|
|
107
107
|
await expect(invokeFetchWithCache(testDataProvider, mockUrl)).rejects.toThrow('API call failed');
|
|
108
|
-
expect(logger.error).toHaveBeenCalledWith(
|
|
108
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
109
|
+
`Error fetching ${mockUrl}:`,
|
|
110
|
+
expect.objectContaining({ message: 'API call failed' })
|
|
111
|
+
);
|
|
109
112
|
});
|
|
110
113
|
});
|
|
111
114
|
|
|
@@ -1108,7 +1108,7 @@ describe('TicketsDataProvider', () => {
|
|
|
1108
1108
|
expect(() =>
|
|
1109
1109
|
(ticketsDataProvider as any).filterFieldsByColumns(item, columnsToFilterMap, resultedRefNameMap),
|
|
1110
1110
|
).toThrow();
|
|
1111
|
-
expect(logger.error).toHaveBeenCalledWith(
|
|
1111
|
+
expect(logger.error).toHaveBeenCalledWith('Cannot filter columns:', expect.objectContaining({ message: expect.any(String) }));
|
|
1112
1112
|
});
|
|
1113
1113
|
});
|
|
1114
1114
|
|
|
@@ -2779,7 +2779,7 @@ describe('TicketsDataProvider', () => {
|
|
|
2779
2779
|
expect(res.categories['Security and Privacy Requirements']).toHaveLength(1);
|
|
2780
2780
|
expect(res.categories['Precedence and Criticality of Requirements']).toHaveLength(1);
|
|
2781
2781
|
expect(res.categories['Other Requirements']).toHaveLength(1);
|
|
2782
|
-
expect(logger.warn).toHaveBeenCalledWith(
|
|
2782
|
+
expect(logger.warn).toHaveBeenCalledWith('Could not fetch work item 13:', expect.objectContaining({ message: 'boom' }));
|
|
2783
2783
|
});
|
|
2784
2784
|
|
|
2785
2785
|
it('should extract IDs from workItemRelations for OneHop queries', async () => {
|