UNPKG

@deployport/specular-runtime

Version:

Runtime for Specular API clients with support for Node.js and Browser

359 lines (341 loc) 13.5 kB
import test from 'tape'; import { SpecularPackage, ResponseMeta, BodyMeta, BodyType, NotFoundProblem, NotFoundProblemMeta } from './generated-client/specular.js'; import { Metadata, } from '../lib/index.js'; import { StructPath } from '../lib/metadata/struct.js'; import { defaultZeroTime } from '../lib/metadata/builtin.js'; import { BlobToBase64 } from '../lib/runtime/builtin.js'; import { parseHTTPResult } from '../lib/runtime/client.js'; import { RpcError, UnknownRpcError } from '../lib/runtime/error.js'; import { TypeNotFoundError } from '../lib/metadata/package.js'; const _pkg = SpecularPackage(); test('StructPath', (t) => { const sp = StructPath.fromString('application/spec.myns.mymod.mytype'); t.equal(sp.name, "mytype"); t.equal(sp.module.namespace, "myns"); t.equal(sp.module.name, "mymod"); t.end(); }); test('Deserialize with builtins props', (t) => { t.test("missing", (t) => { const obj = { "body": {} }; const s = _pkg.requireBuildFromJSON(ResponseMeta.path, obj); t.notEqual(s.body, null); t.equal(s.body.contentLengthInt32, 0); t.equal(s.body.contentLengthInt64, 0); t.equal(s.body.contentLengthFloat32, 0); t.equal(s.body.contentLengthFloat64, 0); t.equal(s.body.contentLengthFloat64Nullable, null); t.equal(s.body.contentLengthUint32, 0); t.equal(s.body.contentLengthUint64, 0); t.equal(s.body.messageString, ""); t.equal(s.body.messageStringNullable, null); t.equal(s.body.enabledBool, false, "default bool value when missing"); t.equal(s.body.enabledBoolNullable, null); t.deepEqual(s.body.createdAt, defaultZeroTime()); t.deepEqual(s.body.createdAtNullable, null); t.notEqual(s.body.fileData, null); t.equal(typeof s.body.fileData, 'object'); t.equal(s.body.fileData.constructor, Blob); t.equal(s.body.fileDataNullable, null); t.equal(s.body.bodyType, BodyType.Normal, "default enum value when missing"); t.equal(s.body.bodyTypeNullable, null); t.end(); }); t.test("value", (t) => { const obj = { "body": { "contentLengthInt32": 32, "contentLengthFloat64Nullable": 64, "messageString": "msg", "messageStringNullable": "msg nullable", "enabledBool": true, "enabledBoolNullable": true, "createdAtNullable": "2024-06-18T02:02:07.602Z", "fileData": "", "bodyType": "special", "bodyTypeNullable": "special" } }; const s = _pkg.requireBuildFromJSON(ResponseMeta.path, obj); t.notEqual(s.body, null); t.equal(s.body.contentLengthInt32, 32); t.equal(s.body.contentLengthFloat64Nullable, 64); t.equal(s.body.messageString, "msg"); t.equal(s.body.messageStringNullable, "msg nullable"); t.equal(s.body.enabledBool, true); t.equal(s.body.enabledBoolNullable, true); t.same(s.body.createdAtNullable, new Date('2024-06-18T02:02:07.602Z')); t.same(s.body.fileData, new Blob([])); t.equal(s.body.bodyType, BodyType.Special); t.equal(s.body.bodyTypeNullable, BodyType.Special); t.end(); }); t.test("value binary", async (t) => { const obj = { "body": { "fileData": await BlobToBase64(new Blob(['hi'], { type: "text/plain" })), } }; const s = _pkg.requireBuildFromJSON(ResponseMeta.path, obj); t.notEqual(s.body, null); t.notEqual(s.body.fileData, null); t.true(areArrayBuffersEqual(await s.body.fileData.arrayBuffer(), await new Blob(['hi'], { type: "text/plain" }).arrayBuffer())); t.end(); }); t.test("nullable binary null", async (t) => { const obj = { "body": { "fileDataNullable": null, } }; const s = _pkg.requireBuildFromJSON(ResponseMeta.path, obj); t.notEqual(s.body, null); t.equal(s.body.fileDataNullable, null); t.end(); }); t.test("nullable binary value", async (t) => { const obj = { "body": { "fileDataNullable": await BlobToBase64(new Blob(['hi'], { type: "text/plain" })), } }; const s = _pkg.requireBuildFromJSON(ResponseMeta.path, obj); t.notEqual(s.body, null); t.notEqual(s.body.fileDataNullable, null); t.true(areArrayBuffersEqual(await s.body.fileDataNullable.arrayBuffer(), await new Blob(['hi'], { type: "text/plain" }).arrayBuffer())); t.end(); }); }); function areArrayBuffersEqual(buffer1, buffer2) { if (buffer1.byteLength !== buffer2.byteLength) return false; const view1 = new Uint8Array(buffer1); const view2 = new Uint8Array(buffer2); for (let i = 0; i < view1.length; i++) { if (view1[i] !== view2[i]) return false; } return true; } test('Deserialize with struct props', (t) => { t.test("null field", (t) => { const obj = { "body": null }; const s = _pkg.requireBuildFromJSON(ResponseMeta.path, obj); t.equal(s.body, null); t.end(); }); t.test("missing field", (t) => { const obj = {}; const s = _pkg.requireBuildFromJSON(ResponseMeta.path, obj); console.log("s", s) t.equal(s.body, null); t.end(); }); }); test('Serialize with struct props', (t) => { t.test("missing", async (t) => { const obj = await ResponseMeta.serialize({}) t.deepEqual(Object.keys(obj), []) t.end() }); t.test("struct prop null", async (t) => { const obj = await ResponseMeta.serialize({ body: null }) t.deepEqual(Object.keys(obj), []) t.end() }); t.test("struct prop empty", async (t) => { const obj = await ResponseMeta.serialize({ body: {} }) t.false(Object.keys(obj).indexOf('body'), -1) t.end() }); }); test('Serialize with builtin props', (t) => { t.test("missing", async (t) => { const obj = await BodyMeta.serialize({}) t.deepEqual(Object.keys(obj), []) t.end() }); t.test("custom value", async (t) => { const obj = await BodyMeta.serialize({ contentLengthInt32: 32, contentLengthUint64Nullable: 64, messageString: 'msg', enabledBool: true, }) const props = Object.keys(obj); t.true(props.includes('contentLengthInt32')) t.false(props.includes('contentLengthFloat64Nullable'), 'should not include default nullable null') t.equal(obj.contentLengthInt32, 32); t.equal(obj.contentLengthUint64Nullable, 64); t.equal(obj.messageString, 'msg'); t.true(props.includes('enabledBool')) t.equal(obj.enabledBool, true); t.end() }); t.test("bool false is omitted", async (t) => { const obj = await BodyMeta.serialize({ enabledBool: false, enabledBoolNullable: false, }) const props = Object.keys(obj); t.false(props.includes('enabledBool'), 'false should be omitted (zero value)') t.false(props.includes('enabledBoolNullable'), 'nullable false should be omitted too') t.end() }); t.test("nulls", async (t) => { const obj = await BodyMeta.serialize({ messageStringNullable: null, contentLengthFloat64Nullable: null, fileDataNullable: null, bodyTypeNullable: null, }) const props = Object.keys(obj); t.false(props.includes('messageStringNullable')) t.false(props.includes('contentLengthFloat64Nullable'), 'should not include default nullable null') t.false(props.includes('fileDataNullable'), 'should not include default nullable null') t.false(props.includes('bodyTypeNullable'), 'should not include default nullable null') t.end() }); t.test("nullable set", async (t) => { const obj = await BodyMeta.serialize({ messageStringNullable: 'val', bodyTypeNullable: 'special', }) const props = Object.keys(obj); t.true(props.includes('messageStringNullable')) t.equal(obj.messageStringNullable, 'val') t.equal(obj.bodyTypeNullable, 'special') t.end() }); t.test("time default", async (t) => { const obj = await BodyMeta.serialize({ createdAt: defaultZeroTime(), }) const props = Object.keys(obj); t.false(props.includes('createdAt'), 'should not include date since is default date zero value') t.deepEqual(props, []); t.end() }); t.test("time custom", async (t) => { const obj = await BodyMeta.serialize({ createdAt: new Date(Date.UTC(2024, 1, 1)), }) const props = Object.keys(obj); t.true(props.includes('createdAt')) t.deepEqual(props, ['createdAt']); t.equal(obj.createdAt, '2024-02-01T00:00:00.000Z'); t.end() }); t.test("binary default", async (t) => { const obj = await BodyMeta.serialize({ fileData: new Blob([''], { type: "text/plain" }), }) const props = Object.keys(obj); t.false(props.includes('fileData'), 'should not include date since is default binary is an empty string') t.deepEqual(props, []); t.end() }); t.test("binary value", async (t) => { const obj = await BodyMeta.serialize({ fileData: new Blob(['hi'], { type: "text/plain" }), fileDataNullable: new Blob(['hi'], { type: "text/plain" }), }) const props = Object.keys(obj); t.true(props.includes('fileData')) t.equal(obj.fileData, 'aGk='); t.equal(obj.fileDataNullable, 'aGk='); t.end() }); t.test("enum default", async (t) => { const obj = await BodyMeta.serialize({ bodyType: BodyType.Normal, }) const props = Object.keys(obj); t.false(props.includes('bodyType'), 'should not include since default value was set') t.deepEqual(props, []); t.end() }); }); const importingPkg = new Metadata.Package( 'specularJS', 'importing', ); importingPkg.importPackage(_pkg); export const ImportingDoc = new Metadata.Struct( importingPkg, "Doc", ); test('imported instantiation', (t) => { const obj = { "body": null }; const s = importingPkg.requireBuildFromJSON(ResponseMeta.path, obj); t.equal(s.body, null); t.end(); }); test('unmatched case instantiation', (t) => { const obj = { "body": null }; const sp = StructPath.fromString(ResponseMeta.path.mediaType.toUpperCase()); const s = _pkg.requireBuildFromJSON(sp, obj); t.equal(s.body, null); t.end(); }); test('unknown error type degrades to UnknownRpcError', async (t) => { const ct = 'application/spec.myns.mymod.futureproblem+json; kind=error'; try { await parseHTTPResult(_pkg, ct, async () => ({ message: 'quota exceeded', code: 'QUOTA', limit: 100 })); t.fail('expected parseHTTPResult to throw'); } catch (e) { t.ok(e instanceof UnknownRpcError, 'is UnknownRpcError'); t.ok(e instanceof RpcError, 'is RpcError'); t.ok(e instanceof Error, 'is Error'); t.equal(e.message, 'quota exceeded'); t.equal(e.code, 'QUOTA'); t.equal(e.typeName, 'application/spec.myns.mymod.futureproblem'); t.deepEqual(e.payload, { message: 'quota exceeded', code: 'QUOTA', limit: 100 }); } t.end(); }); test('deserialized struct preserves its prototype (buildByFQTN does not flatten)', (t) => { const r = _pkg.requireBuildFromJSON(NotFoundProblemMeta.path, { message: 'boom', status: 1 }); t.ok(r instanceof NotFoundProblem, 'instance keeps its NotFoundProblem prototype'); t.ok(r instanceof Error, 'instance is a real Error'); t.equal(r.message, 'boom', 'message preserved'); t.equal(r.status, 1); t.equal(r.__structPath, NotFoundProblemMeta.path, '__structPath preserved'); t.end(); }); test('user-defined error is thrown (not returned) as a real Error', async (t) => { const ct = NotFoundProblemMeta.path.mediaType + '+json; kind=error'; try { await parseHTTPResult(_pkg, ct, async () => ({ message: 'not found', detail: 'x', status: 404, title: 'NF' })); t.fail('expected parseHTTPResult to throw the user-defined error'); } catch (e) { t.ok(e instanceof Error, 'thrown value is a real Error'); t.ok(e instanceof NotFoundProblem, 'thrown value is a NotFoundProblem instance'); t.equal(e.message, 'not found', 'message preserved'); t.equal(e.detail, 'x'); t.equal(e.status, 404); } t.end(); }); test('unknown type without kind=error stays a TypeNotFoundError', async (t) => { const ct = 'application/spec.myns.mymod.futureoutput+json'; try { await parseHTTPResult(_pkg, ct, async () => ({ id: '1' })); t.fail('expected parseHTTPResult to throw'); } catch (e) { t.notOk(e instanceof UnknownRpcError, 'not UnknownRpcError'); t.ok(e instanceof TypeNotFoundError, 'is TypeNotFoundError'); } t.end(); });