@tak-ps/node-tak 12.24.0 → 12.26.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.
@@ -0,0 +1,339 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { Readable } from 'node:stream';
4
+ import { Client } from 'undici';
5
+ import type { Dispatcher } from 'undici';
6
+ import TAKAPI from '../lib/api.js';
7
+ import { APIAuthCertificate } from '../lib/auth.js';
8
+
9
+ type RequestArgs = Parameters<Client['request']>[0];
10
+
11
+ function mockResponse(statusCode: number, contentType: string, body: string | Buffer): Dispatcher.ResponseData {
12
+ return {
13
+ statusCode,
14
+ headers: { 'content-type': contentType },
15
+ body: Readable.from([Buffer.from(body)]),
16
+ trailers: {}
17
+ } as unknown as Dispatcher.ResponseData;
18
+ }
19
+
20
+ // Self-signed test certificate: O=CO-TAK, OU=WILDFIRE, CN=alice@example.com, expires 2036-08-22
21
+ const FIXTURE_PEM = `-----BEGIN CERTIFICATE-----
22
+ MIIB1TCCAXugAwIBAgIUHPumta9q/l2whKqG1nGD9pJ/uogwCgYIKoZIzj0EAwIw
23
+ QDEPMA0GA1UECgwGQ08tVEFLMREwDwYDVQQLDAhXSUxERklSRTEaMBgGA1UEAwwR
24
+ YWxpY2VAZXhhbXBsZS5jb20wHhcNMjYwODI1MDEwNTQxWhcNMzYwODIyMDEwNTQx
25
+ WjBAMQ8wDQYDVQQKDAZDTy1UQUsxETAPBgNVBAsMCFdJTERGSVJFMRowGAYDVQQD
26
+ DBFhbGljZUBleGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABB45
27
+ OnCT2nVQVxlgKDLttbhLW9Yjs2/elFjAZkil+iI0QLM+vtRjdoCdI+NhADggvJ65
28
+ +++JRYr8Kv2zwv2joSKjUzBRMB0GA1UdDgQWBBS8Depzqkj88/9GNHWY8GmyreFB
29
+ VTAfBgNVHSMEGDAWgBS8Depzqkj88/9GNHWY8GmyreFBVTAPBgNVHRMBAf8EBTAD
30
+ AQH/MAoGCCqGSM49BAMCA0gAMEUCIQCU1mg7e/Eu4A84SemDc0bIQHTnM9aVP8h1
31
+ LaburcgKWwIgYeKSRCQGrM1cEhJQbqJC6UWpJUGxGcclcMzGjqx7WpQ=
32
+ -----END CERTIFICATE-----`;
33
+ const FIXTURE_FINGERPRINT = '37:CA:5A:CD:2E:AC:F5:41:09:4B:DE:21:34:9F:A8:83:C4:43:20:71:7E:7F:CC:EC:97:2C:9E:AE:E9:5F:83:40';
34
+ const GET_PATH = `/Marti/api/certadmin/cert/${FIXTURE_FINGERPRINT}`;
35
+ const LIST_PATH = '/Marti/api/certadmin/cert?username=alice%40example.com';
36
+
37
+ function certList(certs: object[]): string {
38
+ return JSON.stringify({ version: '3', type: 'TakCert', data: certs, nodeId: 'node' });
39
+ }
40
+
41
+ function certItem(cert: object): string {
42
+ return JSON.stringify({ version: '3', type: 'TakCert', data: cert, nodeId: 'node' });
43
+ }
44
+
45
+ // Tomcat's default page for response.sendError(500) - what the TAK Server
46
+ // returns from getCertificate() when the hash is unknown
47
+ const TOMCAT_500 = '<!doctype html><html lang="en"><head><title>HTTP Status 500 – Internal Server Error</title></head><body><h1>HTTP Status 500 – Internal Server Error</h1></body></html>';
48
+
49
+ async function collect(stream: Readable): Promise<Buffer> {
50
+ const chunks: Buffer[] = [];
51
+ for await (const chunk of stream) chunks.push(Buffer.from(chunk));
52
+ return Buffer.concat(chunks);
53
+ }
54
+
55
+ /**
56
+ * Install a path-keyed mock; returns the list of paths requested in order
57
+ */
58
+ function mockRoutes(routes: Record<string, () => Dispatcher.ResponseData>): { paths: string[]; restore: () => void } {
59
+ const originalRequest = Client.prototype.request;
60
+ const paths: string[] = [];
61
+
62
+ Client.prototype.request = async function(opts: RequestArgs): Promise<Dispatcher.ResponseData> {
63
+ const path = String(opts.path);
64
+ paths.push(path);
65
+ const route = routes[path];
66
+ if (!route) throw new Error(`Unexpected request: ${opts.method ?? 'GET'} ${path}`);
67
+ return route();
68
+ };
69
+
70
+ return { paths, restore: () => { Client.prototype.request = originalRequest; } };
71
+ }
72
+
73
+ function api(): TAKAPI {
74
+ return new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
75
+ }
76
+
77
+ test('Certificate.listActive requests the active certificate list', async () => {
78
+ const mock = mockRoutes({
79
+ '/Marti/api/certadmin/cert/active': () => mockResponse(200, 'application/json', certList([{ id: 1, hash: 'abc', userDn: 'alice' }]))
80
+ });
81
+
82
+ try {
83
+ const res = await api().Certificate.listActive();
84
+
85
+ assert.deepEqual(mock.paths, ['/Marti/api/certadmin/cert/active']);
86
+ assert.equal(res.data.length, 1);
87
+ assert.equal(res.data[0].hash, 'abc');
88
+ } finally {
89
+ mock.restore();
90
+ }
91
+ });
92
+
93
+ test('Certificate.validate resolves a known certificate with a single get(hash) call', async () => {
94
+ const mock = mockRoutes({
95
+ [GET_PATH]: () => mockResponse(200, 'application/json', certItem({ id: 2, hash: FIXTURE_FINGERPRINT, userDn: 'alice@example.com' }))
96
+ });
97
+
98
+ try {
99
+ const res = await api().Certificate.validate(FIXTURE_PEM);
100
+
101
+ assert.deepEqual(mock.paths, [GET_PATH]);
102
+ assert.equal(res.fingerprint, FIXTURE_FINGERPRINT);
103
+ assert.equal(res.username, 'alice@example.com');
104
+ assert.equal(res.expired, false);
105
+ assert.equal(res.known, true);
106
+ assert.equal(res.revoked, false);
107
+ assert.equal(res.revocationDate, undefined);
108
+ assert.equal(res.valid, true);
109
+ assert.equal(res.certificate?.id, 2);
110
+ } finally {
111
+ mock.restore();
112
+ }
113
+ });
114
+
115
+ test('Certificate.validate reports a revoked certificate', async () => {
116
+ const mock = mockRoutes({
117
+ [GET_PATH]: () => mockResponse(200, 'application/json', certItem({ id: 2, hash: FIXTURE_FINGERPRINT, userDn: 'alice@example.com', revocationDate: '2026-08-24T21:00:00.000Z' }))
118
+ });
119
+
120
+ try {
121
+ const res = await api().Certificate.validate(FIXTURE_PEM);
122
+
123
+ assert.equal(res.known, true);
124
+ assert.equal(res.revoked, true);
125
+ assert.equal(res.revocationDate, '2026-08-24T21:00:00.000Z');
126
+ assert.equal(res.expired, false);
127
+ assert.equal(res.valid, false);
128
+ } finally {
129
+ mock.restore();
130
+ }
131
+ });
132
+
133
+ test('Certificate.validate falls back to the user list when get(hash) answers 500 and matches case-insensitively', async () => {
134
+ const mock = mockRoutes({
135
+ [GET_PATH]: () => mockResponse(500, 'text/html', TOMCAT_500),
136
+ [LIST_PATH]: () => mockResponse(200, 'application/json', certList([
137
+ { id: 1, hash: 'AA:BB', userDn: 'alice@example.com', revocationDate: '2026-01-01T00:00:00.000Z' },
138
+ { id: 2, hash: FIXTURE_FINGERPRINT.toLowerCase(), userDn: 'alice@example.com' }
139
+ ]))
140
+ });
141
+
142
+ try {
143
+ const res = await api().Certificate.validate(FIXTURE_PEM);
144
+
145
+ assert.deepEqual(mock.paths, [GET_PATH, LIST_PATH]);
146
+ assert.equal(res.known, true);
147
+ assert.equal(res.revoked, false);
148
+ assert.equal(res.valid, true);
149
+ assert.equal(res.certificate?.id, 2);
150
+ } finally {
151
+ mock.restore();
152
+ }
153
+ });
154
+
155
+ test('Certificate.validate reports an unknown certificate as not revoked but not known', async () => {
156
+ const mock = mockRoutes({
157
+ [GET_PATH]: () => mockResponse(500, 'text/html', TOMCAT_500),
158
+ [LIST_PATH]: () => mockResponse(200, 'application/json', certList([]))
159
+ });
160
+
161
+ try {
162
+ const res = await api().Certificate.validate(FIXTURE_PEM);
163
+
164
+ assert.deepEqual(mock.paths, [GET_PATH, LIST_PATH]);
165
+ assert.equal(res.known, false);
166
+ assert.equal(res.revoked, false);
167
+ assert.equal(res.certificate, undefined);
168
+ assert.equal(res.valid, true);
169
+ } finally {
170
+ mock.restore();
171
+ }
172
+ });
173
+
174
+ test('Certificate.validate rethrows non-500 failures from get(hash) without consulting the list', async () => {
175
+ const mock = mockRoutes({
176
+ [GET_PATH]: () => mockResponse(403, 'text/plain', 'Forbidden')
177
+ });
178
+
179
+ try {
180
+ await assert.rejects(() => api().Certificate.validate(FIXTURE_PEM), (err: unknown) => {
181
+ assert.equal((err as { status?: number }).status, 403);
182
+ return true;
183
+ });
184
+ assert.deepEqual(mock.paths, [GET_PATH]);
185
+ } finally {
186
+ mock.restore();
187
+ }
188
+ });
189
+
190
+ test('Certificate.validate evaluates expiry locally against the supplied clock', async () => {
191
+ const mock = mockRoutes({
192
+ [GET_PATH]: () => mockResponse(200, 'application/json', certItem({ id: 2, hash: FIXTURE_FINGERPRINT, userDn: 'alice@example.com' }))
193
+ });
194
+
195
+ try {
196
+ const res = await api().Certificate.validate(FIXTURE_PEM, { now: new Date('2040-01-01T00:00:00Z') });
197
+
198
+ assert.equal(res.expired, true);
199
+ assert.equal(res.revoked, false);
200
+ assert.equal(res.valid, false);
201
+ } finally {
202
+ mock.restore();
203
+ }
204
+ });
205
+
206
+ test('Certificate.validate rejects a PEM that is not a certificate', async () => {
207
+ await assert.rejects(() => api().Certificate.validate('not a certificate'));
208
+ });
209
+
210
+ test('Certificate.probe reports accepted credentials with the server version', async () => {
211
+ const mock = mockRoutes({
212
+ '/Marti/api/version': () => mockResponse(200, 'text/plain', 'TAK Server 5.4-RELEASE-1\n')
213
+ });
214
+
215
+ try {
216
+ const res = await api().Certificate.probe();
217
+
218
+ assert.deepEqual(mock.paths, ['/Marti/api/version']);
219
+ assert.deepEqual(res, { accepted: true, version: 'TAK Server 5.4-RELEASE-1' });
220
+ } finally {
221
+ mock.restore();
222
+ }
223
+ });
224
+
225
+ test('Certificate.probe classifies a RevokedException error page as revoked', async () => {
226
+ const mock = mockRoutes({
227
+ '/Marti/api/version': () => mockResponse(500, 'text/html', [
228
+ '<!doctype html><html lang="en"><head><title>HTTP Status 500 – Internal Server Error</title></head><body>',
229
+ '<h1>HTTP Status 500 – Internal Server Error</h1><p><b>Type</b> Exception Report</p>',
230
+ '<p><b>Message</b> Exception performing TAK Server authentication</p>',
231
+ '<p><b>Exception</b></p><pre>org.springframework.security.authentication.BadCredentialsException: Exception performing TAK Server authentication</pre>',
232
+ '<p><b>Root Cause</b></p><pre>com.bbn.marti.remote.exception.RevokedException: Attempt to use revoked certificate : CN=alice@example.com,OU=WILDFIRE,O=CO-TAK</pre>',
233
+ '</body></html>'
234
+ ].join(''))
235
+ });
236
+
237
+ try {
238
+ const res = await api().Certificate.probe();
239
+
240
+ assert.equal(res.accepted, false);
241
+ assert.equal(res.reason, 'revoked');
242
+ assert.ok(res.message);
243
+ } finally {
244
+ mock.restore();
245
+ }
246
+ });
247
+
248
+ test('Certificate.probe classifies a BadCredentialsException without a revocation cause as rejected', async () => {
249
+ const mock = mockRoutes({
250
+ '/Marti/api/version': () => mockResponse(500, 'text/plain', 'org.springframework.security.authentication.BadCredentialsException: Exception performing TAK Server authentication')
251
+ });
252
+
253
+ try {
254
+ const res = await api().Certificate.probe();
255
+
256
+ assert.equal(res.accepted, false);
257
+ assert.equal(res.reason, 'rejected');
258
+ } finally {
259
+ mock.restore();
260
+ }
261
+ });
262
+
263
+ test('Certificate.probe classifies a TLS handshake failure as tls', async () => {
264
+ const originalRequest = Client.prototype.request;
265
+
266
+ Client.prototype.request = async function(): Promise<Dispatcher.ResponseData> {
267
+ const err = new Error('fetch failed') as Error & { cause?: Error };
268
+ err.cause = Object.assign(new Error('write EPROTO SSL routines:ssl3_read_bytes:sslv3 alert bad certificate'), { code: 'EPROTO' });
269
+ throw err;
270
+ };
271
+
272
+ try {
273
+ const res = await api().Certificate.probe();
274
+
275
+ assert.equal(res.accepted, false);
276
+ assert.equal(res.reason, 'tls');
277
+ assert.match(res.message ?? '', /EPROTO/);
278
+ } finally {
279
+ Client.prototype.request = originalRequest;
280
+ }
281
+ });
282
+
283
+ test('Certificate.probe rethrows errors that are not authentication failures', async () => {
284
+ const mock = mockRoutes({
285
+ '/Marti/api/version': () => mockResponse(503, 'text/plain', 'Service Unavailable')
286
+ });
287
+
288
+ try {
289
+ await assert.rejects(() => api().Certificate.probe(), (err: unknown) => {
290
+ assert.equal((err as { status?: number }).status, 503);
291
+ return true;
292
+ });
293
+ } finally {
294
+ mock.restore();
295
+ }
296
+ });
297
+
298
+ test('Certificate.downloadIds streams the ZIP archive for the given IDs', async () => {
299
+ const mock = mockRoutes({
300
+ '/Marti/api/certadmin/cert/download/1,2,3': () => mockResponse(200, 'application/zip', Buffer.from('zip-bytes'))
301
+ });
302
+
303
+ try {
304
+ const body = await collect(await api().Certificate.downloadIds([1, '2', 3]));
305
+
306
+ assert.deepEqual(mock.paths, ['/Marti/api/certadmin/cert/download/1,2,3']);
307
+ assert.equal(body.toString(), 'zip-bytes');
308
+ } finally {
309
+ mock.restore();
310
+ }
311
+ });
312
+
313
+ test('Certificate.downloadIds rejects an empty ID list without a request', async () => {
314
+ const mock = mockRoutes({});
315
+
316
+ try {
317
+ await assert.rejects(() => api().Certificate.downloadIds([]), /At least one ID must be provided/);
318
+ assert.deepEqual(mock.paths, []);
319
+ } finally {
320
+ mock.restore();
321
+ }
322
+ });
323
+
324
+ test('Certificate.downloadIds throws on a TAK Server error instead of streaming the error page', async () => {
325
+ const mock = mockRoutes({
326
+ '/Marti/api/certadmin/cert/download/1': () => mockResponse(500, 'text/plain', 'exception in downloadCertificates!')
327
+ });
328
+
329
+ try {
330
+ await assert.rejects(() => api().Certificate.downloadIds([1]), (err: unknown) => {
331
+ assert.ok(err instanceof Error);
332
+ assert.equal((err as { status?: number }).status, 500);
333
+ assert.match(err.message, /downloadCertificates/);
334
+ return true;
335
+ });
336
+ } finally {
337
+ mock.restore();
338
+ }
339
+ });
@@ -0,0 +1,139 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import Err from '@openaddresses/batch-error';
4
+ import {
5
+ TAKServerError,
6
+ isHTML,
7
+ parseHTMLError,
8
+ summarizeHTMLError,
9
+ formatHTMLError,
10
+ } from '../lib/utils/html-error.js';
11
+
12
+ const TOMCAT = `<!doctype html><html lang="en"><head><title>HTTP Status 500 &ndash; Internal Server Error</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 500 &ndash; Internal Server Error</h1><hr class="line" /><p><b>Type</b> Exception Report</p><p><b>Message</b> Exception performing TAK Server authentication</p><p><b>Description</b> The server encountered an unexpected condition that prevented it from fulfilling the request.</p><p><b>Exception</b></p><pre>org.springframework.security.authentication.BadCredentialsException: Exception performing TAK Server authentication
13
+ \tcom.bbn.marti.util.spring.TakAuthenticationProvider.authenticateCore(TakAuthenticationProvider.java:190)
14
+ \tcom.bbn.marti.util.spring.TakAuthenticationProvider.authenticate(TakAuthenticationProvider.java:244)
15
+ \torg.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:181)
16
+ </pre><p><b>Note</b> The full stack trace of the root cause is available in the server logs.</p><hr class="line" /><h3>Apache Tomcat/9.0.86</h3></body></html>`;
17
+
18
+ test('html-error: isHTML', () => {
19
+ assert.equal(isHTML(TOMCAT), true);
20
+ assert.equal(isHTML(' <html><body>x</body></html>'), true);
21
+ assert.equal(isHTML('<h1>HTTP Status 404 – Not Found</h1>'), true);
22
+ assert.equal(isHTML('{"message":"nope"}'), false);
23
+ assert.equal(isHTML('Plain text failure'), false);
24
+ assert.equal(isHTML(null), false);
25
+ assert.equal(isHTML({}), false);
26
+ });
27
+
28
+ test('html-error: entities are decoded', () => {
29
+ const parsed = parseHTMLError('<html><body><p><b>Message</b> a &ndash; b &amp; &lt;c&gt; &#39;d&#39; &#x41;</p></body></html>');
30
+ assert.ok(parsed);
31
+ assert.equal(parsed.fields.Message, 'a – b & <c> \'d\' A');
32
+ });
33
+
34
+ test('html-error: malformed HTML strips hidden blocks & unterminated tags', () => {
35
+ const parsed = parseHTMLError('<html><script>let a = "<b>";</script><body><STYLE>b {x:y}</STYLE><p class="x">Gateway <b>Timeout</b><script>never closed');
36
+ assert.ok(parsed);
37
+ assert.equal(parsed.text, 'Gateway Timeout');
38
+ assert.equal(parseHTMLError('<html><body>Broken <tag')?.text, 'Broken');
39
+
40
+ // Pathological input scales linearly (CodeQL polynomial regex finding)
41
+ const time = (n: number) => {
42
+ const big = '<html>' + '<head\t'.repeat(n) + '<'.repeat(n);
43
+ const start = performance.now();
44
+ assert.ok(parseHTMLError(big));
45
+ return performance.now() - start;
46
+ };
47
+ time(1000); // warm up
48
+ const small = time(10000);
49
+ const large = time(40000);
50
+ assert.ok(large < Math.max(small * 10, 50), `4x input took ${large}ms vs ${small}ms`);
51
+ });
52
+
53
+ test('html-error: TAKServerError from non-HTML body', () => {
54
+ const err = new TAKServerError(502, 'upstream <b>down</b>');
55
+ assert.equal(err.message, 'upstream down');
56
+ assert.equal(err.details, 'upstream down');
57
+ });
58
+
59
+ test('html-error: malformed HTML falls back to visible text', () => {
60
+ const parsed = parseHTMLError('<html><head><style>b {x:y}</style></head><body><p>Bad Gateway<br>upstream unavailable</body></html>');
61
+ assert.ok(parsed);
62
+ assert.equal(parsed.title, undefined);
63
+ assert.equal(parsed.text, 'Bad Gateway upstream unavailable');
64
+ assert.equal(summarizeHTMLError(parsed, 502), 'Bad Gateway upstream unavailable');
65
+ });
66
+
67
+ test('html-error: parseHTMLError - Tomcat Exception Report', () => {
68
+ const parsed = parseHTMLError(TOMCAT);
69
+ assert.ok(parsed);
70
+
71
+ assert.equal(parsed.title, 'HTTP Status 500 – Internal Server Error');
72
+ assert.deepEqual(parsed.fields, {
73
+ Type: 'Exception Report',
74
+ Message: 'Exception performing TAK Server authentication',
75
+ Description: 'The server encountered an unexpected condition that prevented it from fulfilling the request.',
76
+ Note: 'The full stack trace of the root cause is available in the server logs.',
77
+ });
78
+
79
+ assert.ok(parsed.exception);
80
+ assert.ok(parsed.exception.startsWith('org.springframework.security.authentication.BadCredentialsException: Exception performing TAK Server authentication\n'));
81
+ assert.ok(parsed.exception.includes('TakAuthenticationProvider.java:244'));
82
+ assert.ok(!parsed.exception.endsWith('\n'));
83
+
84
+ // Styles & exception body are excluded from the text fallback
85
+ assert.ok(!parsed.text.includes('font-family'));
86
+ assert.ok(!parsed.text.includes('TakAuthenticationProvider'));
87
+ assert.ok(parsed.text.includes('Apache Tomcat/9.0.86'));
88
+
89
+ assert.equal(summarizeHTMLError(parsed), 'Exception performing TAK Server authentication');
90
+
91
+ const formatted = formatHTMLError(parsed);
92
+ assert.equal(formatted.split('\n').slice(0, 6).join('\n'), [
93
+ 'HTTP Status 500 – Internal Server Error',
94
+ 'Type: Exception Report',
95
+ 'Message: Exception performing TAK Server authentication',
96
+ 'Description: The server encountered an unexpected condition that prevented it from fulfilling the request.',
97
+ 'Note: The full stack trace of the root cause is available in the server logs.',
98
+ '',
99
+ ].join('\n'));
100
+ assert.ok(formatted.endsWith('ProviderManager.authenticate(ProviderManager.java:181)'));
101
+ });
102
+
103
+ test('html-error: parseHTMLError - status page without Message', () => {
104
+ const parsed = parseHTMLError('<!doctype html><html><head><title>HTTP Status 404 &ndash; Not Found</title></head><body><h1>HTTP Status 404 &ndash; Not Found</h1><p><b>Type</b> Status Report</p><p><b>Description</b> The origin server did not find a current representation for the target resource.</p></body></html>');
105
+ assert.ok(parsed);
106
+ assert.equal(summarizeHTMLError(parsed, 404), 'The origin server did not find a current representation for the target resource.');
107
+ });
108
+
109
+ test('html-error: parseHTMLError - exception only', () => {
110
+ const parsed = parseHTMLError('<html><body><pre>java.lang.IllegalStateException: Mission already exists</pre></body></html>');
111
+ assert.ok(parsed);
112
+ assert.equal(summarizeHTMLError(parsed), 'Mission already exists');
113
+ assert.equal(formatHTMLError(parsed), 'java.lang.IllegalStateException: Mission already exists');
114
+ });
115
+
116
+ test('html-error: parseHTMLError - unstructured HTML', () => {
117
+ const parsed = parseHTMLError('<html><body><div>Service Unavailable</div></body></html>');
118
+ assert.ok(parsed);
119
+ assert.equal(parsed.title, undefined);
120
+ assert.equal(summarizeHTMLError(parsed, 503), 'Service Unavailable');
121
+ assert.equal(formatHTMLError(parsed), 'Service Unavailable');
122
+
123
+ assert.equal(parseHTMLError('not html'), null);
124
+ });
125
+
126
+ test('html-error: TAKServerError', () => {
127
+ const err = new TAKServerError(500, TOMCAT);
128
+
129
+ assert.ok(err instanceof Err);
130
+ assert.ok(err instanceof Error);
131
+ assert.equal(err.name, 'PublicError');
132
+ assert.equal(err.status, 500);
133
+ assert.equal(err.message, 'Exception performing TAK Server authentication');
134
+ assert.equal(err.safe, 'Exception performing TAK Server authentication');
135
+ assert.ok(err.details.startsWith('HTTP Status 500 – Internal Server Error\nType: Exception Report\n'));
136
+ assert.ok(err.details.includes('BadCredentialsException'));
137
+ assert.equal(err.html, TOMCAT);
138
+ assert.equal(err.parsed.fields.Type, 'Exception Report');
139
+ });