@tak-ps/node-tak 12.9.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 +4 -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/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 -1
- package/test/uploadPackage.test.ts +62 -0
package/CHANGELOG.md
CHANGED
package/dist/lib/auth.js
CHANGED
|
@@ -2,6 +2,7 @@ import fetch from './fetch.js';
|
|
|
2
2
|
import { Type } from '@sinclair/typebox';
|
|
3
3
|
import { Client } from 'undici';
|
|
4
4
|
import stream2buffer from './stream.js';
|
|
5
|
+
import { encodeMultipartFormData } from './multipart.js';
|
|
5
6
|
/**
|
|
6
7
|
* Store the TAK Client Certificate for a connection
|
|
7
8
|
*/
|
|
@@ -76,6 +77,17 @@ export class APIAuthCertificate extends APIAuth {
|
|
|
76
77
|
rejectUnauthorized: false,
|
|
77
78
|
}
|
|
78
79
|
});
|
|
80
|
+
if (typeof FormData !== 'undefined' && opts.body instanceof FormData) {
|
|
81
|
+
const multipart = await encodeMultipartFormData(opts.body);
|
|
82
|
+
opts = {
|
|
83
|
+
...opts,
|
|
84
|
+
headers: {
|
|
85
|
+
...(opts.headers || {}),
|
|
86
|
+
...multipart.headers
|
|
87
|
+
},
|
|
88
|
+
body: multipart.body
|
|
89
|
+
};
|
|
90
|
+
}
|
|
79
91
|
const res = await client.request({
|
|
80
92
|
path: String(url).replace(api.url.origin, ''),
|
|
81
93
|
...opts
|
package/dist/lib/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../lib/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,YAAY,CAAC;AAC/B,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,aAAa,MAAO,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../lib/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
function escapeQuoted(value) {
|
|
4
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
5
|
+
}
|
|
6
|
+
export async function encodeMultipartFormData(form) {
|
|
7
|
+
const boundary = `----node-tak-${crypto.randomUUID()}`;
|
|
8
|
+
const closing = Buffer.from(`--${boundary}--\r\n`);
|
|
9
|
+
const parts = Array.from(form.entries()).map(([name, value]) => {
|
|
10
|
+
if (typeof value === 'string') {
|
|
11
|
+
const header = Buffer.from(`--${boundary}\r\n`
|
|
12
|
+
+ `Content-Disposition: form-data; name="${escapeQuoted(name)}"\r\n\r\n`);
|
|
13
|
+
const body = Buffer.from(value);
|
|
14
|
+
const trailer = Buffer.from('\r\n');
|
|
15
|
+
return {
|
|
16
|
+
header,
|
|
17
|
+
kind: 'buffer',
|
|
18
|
+
body,
|
|
19
|
+
trailer,
|
|
20
|
+
size: header.length + body.length + trailer.length,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const filename = 'name' in value && typeof value.name === 'string'
|
|
24
|
+
? value.name
|
|
25
|
+
: 'blob';
|
|
26
|
+
const type = value.type || 'application/octet-stream';
|
|
27
|
+
const header = Buffer.from(`--${boundary}\r\n`
|
|
28
|
+
+ `Content-Disposition: form-data; name="${escapeQuoted(name)}"; filename="${escapeQuoted(filename)}"\r\n`
|
|
29
|
+
+ `Content-Type: ${type}\r\n\r\n`);
|
|
30
|
+
const trailer = Buffer.from('\r\n');
|
|
31
|
+
return {
|
|
32
|
+
header,
|
|
33
|
+
kind: 'file',
|
|
34
|
+
body: value,
|
|
35
|
+
trailer,
|
|
36
|
+
size: header.length + value.size + trailer.length,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
const contentLength = parts.reduce((sum, part) => sum + part.size, 0) + closing.length;
|
|
40
|
+
const body = Readable.from((async function* () {
|
|
41
|
+
for (const part of parts) {
|
|
42
|
+
yield part.header;
|
|
43
|
+
if (part.kind === 'buffer') {
|
|
44
|
+
yield part.body;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
yield Buffer.from(await part.body.arrayBuffer());
|
|
48
|
+
}
|
|
49
|
+
yield part.trailer;
|
|
50
|
+
}
|
|
51
|
+
yield closing;
|
|
52
|
+
})());
|
|
53
|
+
return {
|
|
54
|
+
body,
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
57
|
+
'Content-Length': String(contentLength),
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=multipart.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multipart.js","sourceRoot":"","sources":["../../lib/multipart.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
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 TAKAPI from '../lib/api.js';
|
|
6
|
+
import { APIAuthCertificate } from '../lib/auth.js';
|
|
7
|
+
import stream2buffer from '../lib/stream.js';
|
|
8
|
+
test('Files.uploadPackage serializes multipart for certificate auth', async () => {
|
|
9
|
+
const originalRequest = Client.prototype.request;
|
|
10
|
+
let captured;
|
|
11
|
+
Client.prototype.request = async function (opts) {
|
|
12
|
+
captured = opts;
|
|
13
|
+
return {
|
|
14
|
+
statusCode: 200,
|
|
15
|
+
headers: {
|
|
16
|
+
'content-type': 'text/plain'
|
|
17
|
+
},
|
|
18
|
+
body: Readable.from([Buffer.from('ok')]),
|
|
19
|
+
trailers: {}
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
try {
|
|
23
|
+
const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
|
|
24
|
+
const res = await api.Files.uploadPackage({
|
|
25
|
+
name: 'mission.zip',
|
|
26
|
+
creatorUid: 'user-1',
|
|
27
|
+
hash: 'hash-123',
|
|
28
|
+
keywords: ['alpha'],
|
|
29
|
+
groups: ['Blue']
|
|
30
|
+
}, Buffer.from('zip-bytes'));
|
|
31
|
+
assert.equal(res, 'ok');
|
|
32
|
+
assert.ok(captured);
|
|
33
|
+
assert.equal(captured.path, '/Marti/sync/missionupload?filename=mission.zip&creatorUid=user-1&hash=hash-123&keyword=missionpackage&keyword=alpha&Groups=Blue');
|
|
34
|
+
const headers = captured.headers;
|
|
35
|
+
assert.equal(typeof headers['Content-Type'], 'string');
|
|
36
|
+
assert.equal(typeof headers['Content-Length'], 'string');
|
|
37
|
+
assert.match(headers['Content-Type'], /^multipart\/form-data; boundary=----node-tak-/);
|
|
38
|
+
assert.equal(Number(headers['Content-Length']) > 0, true);
|
|
39
|
+
assert.ok(captured.body instanceof Readable);
|
|
40
|
+
const body = await stream2buffer(captured.body);
|
|
41
|
+
const multipart = body.toString('utf8');
|
|
42
|
+
assert.match(multipart, /Content-Disposition: form-data; name="assetfile"; filename="mission.zip"/);
|
|
43
|
+
assert.match(multipart, /Content-Type: application\/zip/);
|
|
44
|
+
assert.match(multipart, /zip-bytes/);
|
|
45
|
+
assert.match(multipart, /--$/m);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
Client.prototype.request = originalRequest;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
//# sourceMappingURL=uploadPackage.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uploadPackage.test.js","sourceRoot":"","sources":["../../test/uploadPackage.test.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,OAAO,MAAM,MAAM,eAAe,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,aAAa,MAAM,kBAAkB,CAAC;AAI7C,IAAI,CAAC,+DAA+D,EAAE,KAAK,IAAI,EAAE;IAC7E,MAAM,eAAe,GAAG,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC;IAEjD,IAAI,QAAiC,CAAC;IAEtC,MAAM,CAAC,SAAS,CAAC,OAAO,GAAG,KAAK,WAAU,IAAiB;QACvD,QAAQ,GAAG,IAAI,CAAC;QAEhB,OAAO;YACH,UAAU,EAAE,GAAG;YACf,OAAO,EAAE;gBACL,cAAc,EAAE,YAAY;aAC/B;YACD,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;YACxC,QAAQ,EAAE,EAAE;SACuB,CAAC;IAC5C,CAAC,CAAC;IAEF,IAAI,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC,yBAAyB,CAAC,EAAE,IAAI,kBAAkB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;QAElG,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,KAAK,CAAC,aAAa,CAAC;YACtC,IAAI,EAAE,aAAa;YACnB,UAAU,EAAE,QAAQ;YACpB,IAAI,EAAE,UAAU;YAChB,QAAQ,EAAE,CAAC,OAAO,CAAC;YACnB,MAAM,EAAE,CAAC,MAAM,CAAC;SACnB,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;QAE7B,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACxB,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;QACpB,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,iIAAiI,CAAC,CAAC;QAC/J,MAAM,OAAO,GAAG,QAAQ,CAAC,OAA8B,CAAC;QACxD,MAAM,CAAC,KAAK,CAAC,OAAO,OAAO,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC,CAAC;QACvD,MAAM,CAAC,KAAK,CAAC,OAAO,OAAO,CAAC,gBAAgB,CAAC,EAAE,QAAQ,CAAC,CAAC;QACzD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,cAAc,CAAW,EAAE,+CAA+C,CAAC,CAAC;QACjG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;QAE1D,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,YAAY,QAAQ,CAAC,CAAC;QAC7C,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAChD,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAExC,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,0EAA0E,CAAC,CAAC;QACpG,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,gCAAgC,CAAC,CAAC;QAC1D,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;QACrC,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;YAAS,CAAC;QACP,MAAM,CAAC,SAAS,CAAC,OAAO,GAAG,eAAe,CAAC;IAC/C,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../cli.ts","../index.ts","../test/default.test.ts","../test/findCoT.test.ts","../test/pipeline.test.ts","../test/queue.test.ts","../lib/api.ts","../lib/auth.ts","../lib/commands.ts","../lib/fetch.ts","../lib/stream.ts","../lib/api/certificate.ts","../lib/api/client.ts","../lib/api/contacts.ts","../lib/api/credentials.ts","../lib/api/export.ts","../lib/api/files.ts","../lib/api/groups.ts","../lib/api/iconsets.ts","../lib/api/injectors.ts","../lib/api/locate.ts","../lib/api/mission-invite.ts","../lib/api/mission-layer.ts","../lib/api/mission-log.ts","../lib/api/mission.ts","../lib/api/oauth.ts","../lib/api/package.ts","../lib/api/profile.ts","../lib/api/query.ts","../lib/api/repeater.ts","../lib/api/security.ts","../lib/api/subscriptions.ts","../lib/api/types.ts","../lib/api/video.ts","../lib/utils/queue.ts"],"version":"6.0.2"}
|
|
1
|
+
{"root":["../cli.ts","../index.ts","../test/default.test.ts","../test/findCoT.test.ts","../test/pipeline.test.ts","../test/queue.test.ts","../test/uploadPackage.test.ts","../lib/api.ts","../lib/auth.ts","../lib/commands.ts","../lib/fetch.ts","../lib/multipart.ts","../lib/stream.ts","../lib/api/certificate.ts","../lib/api/client.ts","../lib/api/contacts.ts","../lib/api/credentials.ts","../lib/api/export.ts","../lib/api/files.ts","../lib/api/groups.ts","../lib/api/iconsets.ts","../lib/api/injectors.ts","../lib/api/locate.ts","../lib/api/mission-invite.ts","../lib/api/mission-layer.ts","../lib/api/mission-log.ts","../lib/api/mission.ts","../lib/api/oauth.ts","../lib/api/package.ts","../lib/api/profile.ts","../lib/api/query.ts","../lib/api/repeater.ts","../lib/api/security.ts","../lib/api/subscriptions.ts","../lib/api/types.ts","../lib/api/video.ts","../lib/utils/queue.ts"],"version":"6.0.2"}
|
package/lib/auth.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Type } from '@sinclair/typebox';
|
|
|
3
3
|
import { Client } from 'undici';
|
|
4
4
|
import TAKAPI from './api.js';
|
|
5
5
|
import stream2buffer from './stream.js';
|
|
6
|
+
import { encodeMultipartFormData } from './multipart.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Store the TAK Client Certificate for a connection
|
|
@@ -97,6 +98,18 @@ export class APIAuthCertificate extends APIAuth {
|
|
|
97
98
|
}
|
|
98
99
|
});
|
|
99
100
|
|
|
101
|
+
if (typeof FormData !== 'undefined' && opts.body instanceof FormData) {
|
|
102
|
+
const multipart = await encodeMultipartFormData(opts.body);
|
|
103
|
+
opts = {
|
|
104
|
+
...opts,
|
|
105
|
+
headers: {
|
|
106
|
+
...(opts.headers || {}),
|
|
107
|
+
...multipart.headers
|
|
108
|
+
},
|
|
109
|
+
body: multipart.body
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
100
113
|
const res = await client.request({
|
|
101
114
|
path: String(url).replace(api.url.origin, ''),
|
|
102
115
|
...opts
|
package/lib/multipart.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
|
|
4
|
+
type MultipartBufferPart = {
|
|
5
|
+
header: Buffer;
|
|
6
|
+
kind: 'buffer';
|
|
7
|
+
body: Buffer;
|
|
8
|
+
trailer: Buffer;
|
|
9
|
+
size: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type MultipartFilePart = {
|
|
13
|
+
header: Buffer;
|
|
14
|
+
kind: 'file';
|
|
15
|
+
body: File;
|
|
16
|
+
trailer: Buffer;
|
|
17
|
+
size: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type MultipartPart = MultipartBufferPart | MultipartFilePart;
|
|
21
|
+
|
|
22
|
+
function escapeQuoted(value: string): string {
|
|
23
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function encodeMultipartFormData(form: FormData): Promise<{
|
|
27
|
+
body: Readable;
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': string;
|
|
30
|
+
'Content-Length': string;
|
|
31
|
+
};
|
|
32
|
+
}> {
|
|
33
|
+
const boundary = `----node-tak-${crypto.randomUUID()}`;
|
|
34
|
+
const closing = Buffer.from(`--${boundary}--\r\n`);
|
|
35
|
+
|
|
36
|
+
const parts: MultipartPart[] = Array.from(form.entries()).map(([name, value]) => {
|
|
37
|
+
if (typeof value === 'string') {
|
|
38
|
+
const header = Buffer.from(
|
|
39
|
+
`--${boundary}\r\n`
|
|
40
|
+
+ `Content-Disposition: form-data; name="${escapeQuoted(name)}"\r\n\r\n`
|
|
41
|
+
);
|
|
42
|
+
const body = Buffer.from(value);
|
|
43
|
+
const trailer = Buffer.from('\r\n');
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
header,
|
|
47
|
+
kind: 'buffer',
|
|
48
|
+
body,
|
|
49
|
+
trailer,
|
|
50
|
+
size: header.length + body.length + trailer.length,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const filename = 'name' in value && typeof value.name === 'string'
|
|
55
|
+
? value.name
|
|
56
|
+
: 'blob';
|
|
57
|
+
const type = value.type || 'application/octet-stream';
|
|
58
|
+
const header = Buffer.from(
|
|
59
|
+
`--${boundary}\r\n`
|
|
60
|
+
+ `Content-Disposition: form-data; name="${escapeQuoted(name)}"; filename="${escapeQuoted(filename)}"\r\n`
|
|
61
|
+
+ `Content-Type: ${type}\r\n\r\n`
|
|
62
|
+
);
|
|
63
|
+
const trailer = Buffer.from('\r\n');
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
header,
|
|
67
|
+
kind: 'file',
|
|
68
|
+
body: value,
|
|
69
|
+
trailer,
|
|
70
|
+
size: header.length + value.size + trailer.length,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const contentLength = parts.reduce((sum, part) => sum + part.size, 0) + closing.length;
|
|
75
|
+
|
|
76
|
+
const body = Readable.from((async function*() {
|
|
77
|
+
for (const part of parts) {
|
|
78
|
+
yield part.header;
|
|
79
|
+
|
|
80
|
+
if (part.kind === 'buffer') {
|
|
81
|
+
yield part.body;
|
|
82
|
+
} else {
|
|
83
|
+
yield Buffer.from(await part.body.arrayBuffer());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
yield part.trailer;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
yield closing;
|
|
90
|
+
})());
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
body,
|
|
94
|
+
headers: {
|
|
95
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
96
|
+
'Content-Length': String(contentLength),
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
package/package.json
CHANGED
|
@@ -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
|
+
});
|