@tak-ps/node-tak 12.8.0 → 12.10.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/CHANGELOG.md +8 -0
- package/dist/lib/auth.js +12 -0
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/multipart.d.ts +8 -0
- package/dist/lib/multipart.js +61 -0
- package/dist/lib/multipart.js.map +1 -0
- package/dist/test/default.test.js +5 -5
- package/dist/test/default.test.js.map +1 -1
- package/dist/test/findCoT.test.js +16 -22
- package/dist/test/findCoT.test.js.map +1 -1
- package/dist/test/pipeline.test.js +56 -70
- package/dist/test/pipeline.test.js.map +1 -1
- package/dist/test/queue.test.js +22 -25
- package/dist/test/queue.test.js.map +1 -1
- package/dist/test/uploadPackage.test.d.ts +1 -0
- package/dist/test/uploadPackage.test.js +51 -0
- package/dist/test/uploadPackage.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/lib/auth.ts +13 -0
- package/lib/multipart.ts +99 -0
- package/package.json +1 -3
- package/test/default.test.ts +5 -5
- package/test/findCoT.test.ts +16 -22
- package/test/pipeline.test.ts +56 -71
- package/test/queue.test.ts +22 -25
- package/test/uploadPackage.test.ts +62 -0
package/test/pipeline.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import test from '
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
2
3
|
import { EventEmitter } from 'node:events';
|
|
3
4
|
import TAK, { CoT } from '../index.js';
|
|
4
5
|
import { CoTParser } from '@tak-ps/node-cot';
|
|
@@ -20,26 +21,24 @@ function createFakeSocket(opts: { write?: (...args: unknown[]) => boolean } = {}
|
|
|
20
21
|
});
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
test('write - resolves when all CoTs are queued and sent', async (
|
|
24
|
+
test('write - resolves when all CoTs are queued and sent', async () => {
|
|
24
25
|
const tak = createTAK();
|
|
25
26
|
tak.client = createFakeSocket();
|
|
26
27
|
|
|
27
28
|
const cots = [CoT.ping(), CoT.ping()];
|
|
28
29
|
await tak.write(cots);
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
t.end();
|
|
31
|
+
assert.equal(tak.queue.length, 0, 'queue empty after write + pull');
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
test('write - returns early when destroyed', async (
|
|
34
|
+
test('write - returns early when destroyed', async () => {
|
|
35
35
|
const tak = createTAK();
|
|
36
36
|
tak.destroyed = true;
|
|
37
37
|
await tak.write([CoT.ping()]);
|
|
38
|
-
|
|
39
|
-
t.end();
|
|
38
|
+
assert.equal(tak.queue.length, 0, 'nothing queued');
|
|
40
39
|
});
|
|
41
40
|
|
|
42
|
-
test('write - multiple concurrent writes', async (
|
|
41
|
+
test('write - multiple concurrent writes', async () => {
|
|
43
42
|
const tak = createTAK();
|
|
44
43
|
tak.client = createFakeSocket();
|
|
45
44
|
|
|
@@ -48,12 +47,11 @@ test('write - multiple concurrent writes', async (t) => {
|
|
|
48
47
|
const p2 = tak.write([CoT.ping()]).then(() => order.push(2));
|
|
49
48
|
await Promise.all([p1, p2]);
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
t.end();
|
|
50
|
+
assert.deepEqual(order, [1, 2], 'writes resolve in order');
|
|
51
|
+
assert.equal(tak.queue.length, 0, 'queue drained');
|
|
54
52
|
});
|
|
55
53
|
|
|
56
|
-
test('pull - batches CoTs into single socket.write call', async (
|
|
54
|
+
test('pull - batches CoTs into single socket.write call', async () => {
|
|
57
55
|
const tak = createTAK({ socketBatchSize: 64 });
|
|
58
56
|
const writeCalls: string[] = [];
|
|
59
57
|
tak.client = createFakeSocket({
|
|
@@ -65,13 +63,12 @@ test('pull - batches CoTs into single socket.write call', async (t) => {
|
|
|
65
63
|
|
|
66
64
|
await tak.write([CoT.ping(), CoT.ping()]);
|
|
67
65
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
t.end();
|
|
66
|
+
assert.equal(writeCalls.length, 1, 'socket.write called once for batch');
|
|
67
|
+
assert.ok(writeCalls[0].includes('<event'), 'batch contains XML events');
|
|
68
|
+
assert.equal(tak.queue.length, 0, 'queue drained');
|
|
72
69
|
});
|
|
73
70
|
|
|
74
|
-
test('pull - respects backpressure and resumes on process()', async (
|
|
71
|
+
test('pull - respects backpressure and resumes on process()', async () => {
|
|
75
72
|
const tak = createTAK({ socketBatchSize: 1 });
|
|
76
73
|
let callCount = 0;
|
|
77
74
|
const socket = createFakeSocket({
|
|
@@ -89,19 +86,18 @@ test('pull - respects backpressure and resumes on process()', async (t) => {
|
|
|
89
86
|
// Both CoTs get queued immediately, pull() sends 1 and hits backpressure
|
|
90
87
|
await tak.write([CoT.ping(), CoT.ping()]);
|
|
91
88
|
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
assert.equal(callCount, 1, 'one write before backpressure');
|
|
90
|
+
assert.equal(tak.queue.length, 1, 'one item still in queue');
|
|
94
91
|
|
|
95
92
|
// Simulate backpressure clearing (equivalent to drain event)
|
|
96
93
|
socket.writableNeedDrain = false;
|
|
97
94
|
tak.process();
|
|
98
95
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
t.end();
|
|
96
|
+
assert.equal(callCount, 2, 'second write after backpressure cleared');
|
|
97
|
+
assert.equal(tak.queue.length, 0, 'queue drained');
|
|
102
98
|
});
|
|
103
99
|
|
|
104
|
-
test('pull - error in socket.write triggers destroy + error event', async (
|
|
100
|
+
test('pull - error in socket.write triggers destroy + error event', async () => {
|
|
105
101
|
const tak = createTAK();
|
|
106
102
|
tak.client = createFakeSocket({
|
|
107
103
|
write() { throw new Error('write failed'); },
|
|
@@ -115,13 +111,12 @@ test('pull - error in socket.write triggers destroy + error event', async (t) =>
|
|
|
115
111
|
// write() sees destroyed=true on next check and returns
|
|
116
112
|
await tak.write([CoT.ping()]);
|
|
117
113
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
t.end();
|
|
114
|
+
assert.equal(tak.destroyed, true, 'destroyed after write error');
|
|
115
|
+
assert.ok(errorFired, 'error event fired');
|
|
116
|
+
assert.equal(errorMsg, 'write failed', 'error message matches');
|
|
122
117
|
});
|
|
123
118
|
|
|
124
|
-
test('pull - noop when already writing', (
|
|
119
|
+
test('pull - noop when already writing', () => {
|
|
125
120
|
const tak = createTAK();
|
|
126
121
|
tak.client = createFakeSocket();
|
|
127
122
|
tak.writing = true;
|
|
@@ -131,18 +126,16 @@ test('pull - noop when already writing', (t) => {
|
|
|
131
126
|
// pull() should return immediately since writing=true
|
|
132
127
|
tak.process();
|
|
133
128
|
|
|
134
|
-
|
|
135
|
-
t.end();
|
|
129
|
+
assert.equal(tak.queue.length, 1, 'queue unchanged');
|
|
136
130
|
});
|
|
137
131
|
|
|
138
|
-
test('flush - resolves immediately when nothing queued', async (
|
|
132
|
+
test('flush - resolves immediately when nothing queued', async () => {
|
|
139
133
|
const tak = createTAK();
|
|
140
134
|
await tak.flush();
|
|
141
|
-
|
|
142
|
-
t.end();
|
|
135
|
+
assert.ok(true, 'flush resolved immediately');
|
|
143
136
|
});
|
|
144
137
|
|
|
145
|
-
test('flush - rejects when destroyed mid-flush', async (
|
|
138
|
+
test('flush - rejects when destroyed mid-flush', async () => {
|
|
146
139
|
const tak = createTAK();
|
|
147
140
|
tak.client = createFakeSocket();
|
|
148
141
|
|
|
@@ -155,15 +148,14 @@ test('flush - rejects when destroyed mid-flush', async (t) => {
|
|
|
155
148
|
|
|
156
149
|
try {
|
|
157
150
|
await p;
|
|
158
|
-
|
|
151
|
+
assert.fail('should have rejected');
|
|
159
152
|
} catch (err) {
|
|
160
|
-
|
|
161
|
-
|
|
153
|
+
assert.ok(err instanceof Error, 'error is an Error');
|
|
154
|
+
assert.ok((err as Error).message.includes('destroyed'), 'error mentions destroyed');
|
|
162
155
|
}
|
|
163
|
-
t.end();
|
|
164
156
|
});
|
|
165
157
|
|
|
166
|
-
test('flush - waits for queue to drain via process()', async (
|
|
158
|
+
test('flush - waits for queue to drain via process()', async () => {
|
|
167
159
|
const tak = createTAK({ socketBatchSize: 1 });
|
|
168
160
|
let callCount = 0;
|
|
169
161
|
const socket = createFakeSocket({
|
|
@@ -179,7 +171,7 @@ test('flush - waits for queue to drain via process()', async (t) => {
|
|
|
179
171
|
tak.client = socket;
|
|
180
172
|
|
|
181
173
|
await tak.write([CoT.ping(), CoT.ping()]);
|
|
182
|
-
|
|
174
|
+
assert.equal(tak.queue.length, 1, 'one item in queue after backpressure');
|
|
183
175
|
|
|
184
176
|
// Clear backpressure and drain
|
|
185
177
|
socket.writableNeedDrain = false;
|
|
@@ -187,12 +179,11 @@ test('flush - waits for queue to drain via process()', async (t) => {
|
|
|
187
179
|
tak.process();
|
|
188
180
|
await flushP;
|
|
189
181
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
t.end();
|
|
182
|
+
assert.equal(tak.queue.length, 0, 'queue empty after flush');
|
|
183
|
+
assert.equal(callCount, 2, 'both items sent');
|
|
193
184
|
});
|
|
194
185
|
|
|
195
|
-
test('write - yields when queue is full and resumes after destroy', async (
|
|
186
|
+
test('write - yields when queue is full and resumes after destroy', async () => {
|
|
196
187
|
// writeQueueSize=4 → queue capacity = 4
|
|
197
188
|
const tak = createTAK({ writeQueueSize: 4 });
|
|
198
189
|
|
|
@@ -202,17 +193,16 @@ test('write - yields when queue is full and resumes after destroy', async (t) =>
|
|
|
202
193
|
.then(() => { writeResolved = true; });
|
|
203
194
|
await new Promise(r => setImmediate(r));
|
|
204
195
|
|
|
205
|
-
|
|
206
|
-
|
|
196
|
+
assert.equal(tak.queue.length, 4, 'queue at capacity');
|
|
197
|
+
assert.equal(writeResolved, false, 'write blocked waiting for space');
|
|
207
198
|
|
|
208
199
|
// Destroy unblocks write (destroyed check in loop)
|
|
209
200
|
tak.destroy();
|
|
210
201
|
await p;
|
|
211
|
-
|
|
212
|
-
t.end();
|
|
202
|
+
assert.ok(writeResolved, 'write resolved after destroy');
|
|
213
203
|
});
|
|
214
204
|
|
|
215
|
-
test('write - resumes after queue drains', async (
|
|
205
|
+
test('write - resumes after queue drains', async () => {
|
|
216
206
|
// writeQueueSize=8 → queue capacity = 8, socketBatchSize=4
|
|
217
207
|
const tak = createTAK({ writeQueueSize: 8, socketBatchSize: 4 });
|
|
218
208
|
let callCount = 0;
|
|
@@ -230,12 +220,11 @@ test('write - resumes after queue drains', async (t) => {
|
|
|
230
220
|
CoT.ping(), CoT.ping(), CoT.ping(), CoT.ping(), CoT.ping(),
|
|
231
221
|
]);
|
|
232
222
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
t.end();
|
|
223
|
+
assert.equal(tak.queue.length, 0, 'all drained');
|
|
224
|
+
assert.ok(callCount >= 2, 'multiple socket.write calls needed');
|
|
236
225
|
});
|
|
237
226
|
|
|
238
|
-
test('write - caller can safely modify array after write returns', async (
|
|
227
|
+
test('write - caller can safely modify array after write returns', async () => {
|
|
239
228
|
const tak = createTAK();
|
|
240
229
|
const writeCalls: string[] = [];
|
|
241
230
|
tak.client = createFakeSocket({
|
|
@@ -251,13 +240,12 @@ test('write - caller can safely modify array after write returns', async (t) =>
|
|
|
251
240
|
// Modify the array after write — should not affect what was sent
|
|
252
241
|
cots.length = 0;
|
|
253
242
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
t.end();
|
|
243
|
+
assert.equal(writeCalls.length, 1, 'socket.write was called');
|
|
244
|
+
assert.ok(writeCalls[0].includes('<event'), 'CoT was sent before array was cleared');
|
|
245
|
+
assert.equal(tak.queue.length, 0, 'queue empty');
|
|
258
246
|
});
|
|
259
247
|
|
|
260
|
-
test('write - stripFlow replaces existing flow tags in outbound XML', async (
|
|
248
|
+
test('write - stripFlow replaces existing flow tags in outbound XML', async () => {
|
|
261
249
|
const tak = createTAK();
|
|
262
250
|
const writeCalls: string[] = [];
|
|
263
251
|
tak.client = createFakeSocket({
|
|
@@ -292,30 +280,27 @@ test('write - stripFlow replaces existing flow tags in outbound XML', async (t)
|
|
|
292
280
|
|
|
293
281
|
await tak.write([cot], { stripFlow: true });
|
|
294
282
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
283
|
+
assert.equal(writeCalls.length, 1, 'socket.write was called');
|
|
284
|
+
assert.ok(!writeCalls[0].includes('TAK-Server-test='), 'server flow tag removed from outbound XML');
|
|
285
|
+
assert.ok(writeCalls[0].includes('NodeCoT-'), 'node-cot flow tag added to outbound XML');
|
|
298
286
|
|
|
299
287
|
const outbound = CoTParser.from_xml(writeCalls[0]);
|
|
300
288
|
const outboundFlow = outbound.raw.event.detail?.['_flow-tags_'];
|
|
301
289
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
290
|
+
assert.ok(outboundFlow, 'outbound CoT contains flow tags');
|
|
291
|
+
assert.equal(Object.keys(outboundFlow || {}).length, 1, 'outbound CoT flow object is reset to one initial tag');
|
|
292
|
+
assert.ok(!outboundFlow?.['TAK-Server-test'], 'outbound CoT flow object clears prior TAK Server tags');
|
|
305
293
|
|
|
306
294
|
const feat = await CoTParser.to_geojson(cot);
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
t.end();
|
|
295
|
+
assert.equal(feat.properties.flow?.['TAK-Server-test'], '2026-03-08T04:48:00Z', 'original CoT remains unchanged');
|
|
310
296
|
});
|
|
311
297
|
|
|
312
|
-
test('destroy - sets destroyed and clears ping interval', (
|
|
298
|
+
test('destroy - sets destroyed and clears ping interval', () => {
|
|
313
299
|
const tak = createTAK();
|
|
314
300
|
tak.pingInterval = setInterval(() => {}, 60000);
|
|
315
301
|
|
|
316
302
|
tak.destroy();
|
|
317
303
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
t.end();
|
|
304
|
+
assert.equal(tak.destroyed, true, 'destroyed is true');
|
|
305
|
+
assert.equal(tak.pingInterval, undefined, 'pingInterval cleared');
|
|
321
306
|
});
|
package/test/queue.test.ts
CHANGED
|
@@ -1,49 +1,46 @@
|
|
|
1
|
-
import test from '
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
2
3
|
import { Queue } from '../lib/utils/queue.js';
|
|
3
4
|
|
|
4
|
-
test('Queue - push/pop FIFO order', (
|
|
5
|
+
test('Queue - push/pop FIFO order', () => {
|
|
5
6
|
const q = new Queue<number>(4);
|
|
6
7
|
q.push(1);
|
|
7
8
|
q.push(2);
|
|
8
9
|
q.push(3);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
t.end();
|
|
10
|
+
assert.equal(q.pop(), 1);
|
|
11
|
+
assert.equal(q.pop(), 2);
|
|
12
|
+
assert.equal(q.pop(), 3);
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
test('Queue - capacity enforced', (
|
|
15
|
+
test('Queue - capacity enforced', () => {
|
|
16
16
|
const q = new Queue<number>(2);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
t.end();
|
|
17
|
+
assert.equal(q.push(1), true);
|
|
18
|
+
assert.equal(q.push(2), true);
|
|
19
|
+
assert.equal(q.isFull, true);
|
|
20
|
+
assert.equal(q.push(3), false);
|
|
21
|
+
assert.equal(q.length, 2);
|
|
23
22
|
});
|
|
24
23
|
|
|
25
|
-
test('Queue - pop from empty', (
|
|
24
|
+
test('Queue - pop from empty', () => {
|
|
26
25
|
const q = new Queue<number>(2);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
t.end();
|
|
26
|
+
assert.equal(q.pop(), undefined);
|
|
27
|
+
assert.equal(q.length, 0);
|
|
30
28
|
});
|
|
31
29
|
|
|
32
|
-
test('Queue - ring buffer wraparound', (
|
|
30
|
+
test('Queue - ring buffer wraparound', () => {
|
|
33
31
|
const q = new Queue<number>(3);
|
|
34
32
|
q.push(1);
|
|
35
33
|
q.push(2);
|
|
36
34
|
q.push(3);
|
|
37
35
|
// Pop two — head advances past midpoint
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
assert.equal(q.pop(), 1);
|
|
37
|
+
assert.equal(q.pop(), 2);
|
|
40
38
|
// Push two more — tail wraps around
|
|
41
39
|
q.push(4);
|
|
42
40
|
q.push(5);
|
|
43
41
|
// Remaining items come out in FIFO order
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
t.end();
|
|
42
|
+
assert.equal(q.pop(), 3);
|
|
43
|
+
assert.equal(q.pop(), 4);
|
|
44
|
+
assert.equal(q.pop(), 5);
|
|
45
|
+
assert.equal(q.length, 0);
|
|
49
46
|
});
|
|
@@ -0,0 +1,62 @@
|
|
|
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 { IncomingHttpHeaders } from 'node:http';
|
|
6
|
+
import type { Dispatcher } from 'undici';
|
|
7
|
+
import TAKAPI from '../lib/api.js';
|
|
8
|
+
import { APIAuthCertificate } from '../lib/auth.js';
|
|
9
|
+
import stream2buffer from '../lib/stream.js';
|
|
10
|
+
|
|
11
|
+
type RequestArgs = Parameters<Client['request']>[0];
|
|
12
|
+
|
|
13
|
+
test('Files.uploadPackage serializes multipart for certificate auth', async () => {
|
|
14
|
+
const originalRequest = Client.prototype.request;
|
|
15
|
+
|
|
16
|
+
let captured: RequestArgs | undefined;
|
|
17
|
+
|
|
18
|
+
Client.prototype.request = async function(opts: RequestArgs): Promise<Dispatcher.ResponseData> {
|
|
19
|
+
captured = opts;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
statusCode: 200,
|
|
23
|
+
headers: {
|
|
24
|
+
'content-type': 'text/plain'
|
|
25
|
+
},
|
|
26
|
+
body: Readable.from([Buffer.from('ok')]),
|
|
27
|
+
trailers: {}
|
|
28
|
+
} as unknown as Dispatcher.ResponseData;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
|
|
33
|
+
|
|
34
|
+
const res = await api.Files.uploadPackage({
|
|
35
|
+
name: 'mission.zip',
|
|
36
|
+
creatorUid: 'user-1',
|
|
37
|
+
hash: 'hash-123',
|
|
38
|
+
keywords: ['alpha'],
|
|
39
|
+
groups: ['Blue']
|
|
40
|
+
}, Buffer.from('zip-bytes'));
|
|
41
|
+
|
|
42
|
+
assert.equal(res, 'ok');
|
|
43
|
+
assert.ok(captured);
|
|
44
|
+
assert.equal(captured.path, '/Marti/sync/missionupload?filename=mission.zip&creatorUid=user-1&hash=hash-123&keyword=missionpackage&keyword=alpha&Groups=Blue');
|
|
45
|
+
const headers = captured.headers as IncomingHttpHeaders;
|
|
46
|
+
assert.equal(typeof headers['Content-Type'], 'string');
|
|
47
|
+
assert.equal(typeof headers['Content-Length'], 'string');
|
|
48
|
+
assert.match(headers['Content-Type'] as string, /^multipart\/form-data; boundary=----node-tak-/);
|
|
49
|
+
assert.equal(Number(headers['Content-Length']) > 0, true);
|
|
50
|
+
|
|
51
|
+
assert.ok(captured.body instanceof Readable);
|
|
52
|
+
const body = await stream2buffer(captured.body);
|
|
53
|
+
const multipart = body.toString('utf8');
|
|
54
|
+
|
|
55
|
+
assert.match(multipart, /Content-Disposition: form-data; name="assetfile"; filename="mission.zip"/);
|
|
56
|
+
assert.match(multipart, /Content-Type: application\/zip/);
|
|
57
|
+
assert.match(multipart, /zip-bytes/);
|
|
58
|
+
assert.match(multipart, /--$/m);
|
|
59
|
+
} finally {
|
|
60
|
+
Client.prototype.request = originalRequest;
|
|
61
|
+
}
|
|
62
|
+
});
|