Files

1197 lines
55 KiB
JavaScript

// Tests for pbjs tool targets.
var tape = require("tape");
var path = require("path");
var protobuf = require("..");
var fs = require("fs");
var child_process = require("child_process");
var cliTest = require("./helpers/cli");
tape.test("pbjs generates static code", function(test) {
cliTest(test, function() {
var root = protobuf.loadSync("tests/data/cli/test.proto");
root.resolveAll();
var staticTarget = require("../cli/targets/static");
staticTarget(root, {
create: true,
decode: true,
encode: true,
convert: true,
typeurl: true,
}, function(err, jsCode) {
test.error(err, 'static code generation worked');
// jsCode is the generated code; we'll eval it
// (since this is what we normally do with the code, right?)
// This is a test code. Do not use this in production.
var $protobuf = protobuf;
eval(jsCode);
var OneofContainer = protobuf.roots.default.OneofContainer;
var Message = protobuf.roots.default.Message;
test.ok(OneofContainer, "type is loaded");
test.ok(Message, "type is loaded");
// Check that fromObject and toObject work for plain object
var obj = {
messageInOneof: {
value: 42,
},
regularField: "abc",
enumField: 1,
};
var obj1 = OneofContainer.toObject(OneofContainer.fromObject(obj));
test.deepEqual(obj, obj1, "fromObject and toObject work for plain object");
// Check that dynamic fromObject and toObject work for static instance
var root = protobuf.loadSync("tests/data/cli/test.proto");
var OneofContainerDynamic = root.lookup("OneofContainer");
var instance = new OneofContainer();
instance.messageInOneof = new Message();
instance.messageInOneof.value = 42;
instance.regularField = "abc";
instance.enumField = 1;
var instance1 = OneofContainerDynamic.toObject(OneofContainerDynamic.fromObject(instance));
test.deepEqual(OneofContainer.toObject(instance), instance1, "fromObject and toObject work for instance of the static type");
// Check that getTypeUrl works
var defaultTypeUrl = Message.getTypeUrl();
var customTypeUrl = Message.getTypeUrl("example.com");
test.equal(defaultTypeUrl, "type.googleapis.com/Message", "getTypeUrl returns expected url");
test.equal(customTypeUrl, "example.com/Message", "getTypeUrl returns custom url");
test.end();
});
});
});
tape.test("pbjs generates unsigned fixed64 defaults", function(test) {
cliTest(test, function() {
var root = protobuf.Root.fromJSON({
nested: {
M: {
fields: {
v: {
type: "fixed64",
id: 1,
options: {
default: "11000000000000000001"
}
},
s: {
type: "sfixed64",
id: 2,
options: {
default: "-9000000000000000001"
}
}
}
}
}
});
root.resolveAll();
var staticTarget = require("../cli/targets/static");
staticTarget(root, {}, function(err, jsCode) {
test.error(err, "static code generation worked");
test.ok(
/M\.prototype\.v = \$util\.Long \? \$util\.Long\.fromBits\([^)]*,true\) : 11000000000000000000;/.test(jsCode),
"fixed64 default is emitted as unsigned"
);
test.ok(
/M\.prototype\.s = \$util\.Long \? \$util\.Long\.fromBits\([^)]*,false\) : -9000000000000000000;/.test(jsCode),
"sfixed64 default is emitted as signed"
);
test.end();
});
});
});
tape.test("pbjs generates shadow-safe special floating-point defaults", function(test) {
cliTest(test, function() {
var root = protobuf.parse("syntax = \"proto2\";\
message NaN {\
message Infinity {\
optional double positive = 1 [default = inf];\
optional double negative = 2 [default = -inf];\
optional float not_a_number = 3 [default = nan];\
optional double negative_zero = 4 [default = -0];\
optional double finite = 5 [default = 1.5];\
}\
}").root;
root.resolveAll();
var staticTarget = require("../cli/targets/static");
staticTarget(root, {
create: true,
convert: true,
root: "staticSpecialDefaults"
}, function(err, jsCode) {
test.error(err, "static code generation worked");
if (err) {
test.end();
return;
}
delete protobuf.roots.staticSpecialDefaults;
var $protobuf = protobuf;
eval(jsCode);
var Type = protobuf.roots.staticSpecialDefaults.NaN.Infinity,
message = Type.create();
test.equal(message.positive, Infinity, "generated prototype should expose positive infinity");
test.equal(message.negative, -Infinity, "generated prototype should expose negative infinity");
test.ok(Number.isNaN(message.notANumber), "generated prototype should expose NaN");
test.ok(Object.is(message.negativeZero, -0), "generated prototype should expose negative zero");
test.equal(message.finite, 1.5, "generated prototype should expose a finite value");
var object = Type.toObject(message, { defaults: true });
test.equal(object.positive, Infinity, "generated toObject should preserve a positive infinity default");
test.equal(object.negative, -Infinity, "generated toObject should preserve a negative infinity default");
test.ok(Number.isNaN(object.notANumber), "generated toObject should preserve a NaN default");
test.ok(Object.is(object.negativeZero, -0), "generated toObject should preserve a negative zero default");
test.equal(object.finite, 1.5, "generated toObject should preserve a finite default");
var jsonObject = Type.toObject(message, { defaults: true, json: true });
test.equal(jsonObject.positive, "Infinity", "generated toObject should JSON-convert a positive infinity default");
test.equal(jsonObject.negative, "-Infinity", "generated toObject should JSON-convert a negative infinity default");
test.equal(jsonObject.notANumber, "NaN", "generated toObject should JSON-convert a NaN default");
test.ok(Object.is(jsonObject.negativeZero, -0), "generated toObject should preserve a negative zero default in JSON mode");
delete protobuf.roots.staticSpecialDefaults;
test.end();
});
});
});
tape.test("pbjs static service methods expose rpc metadata", function(test) {
cliTest(test, function() {
var root = protobuf.parse([
"syntax = \"proto3\";",
"package rpcmeta;",
"service TestService {",
" rpc Unary (Request) returns (Response);",
" rpc ServerStream (Request) returns (stream Response);",
" rpc ClientStream (stream Request) returns (Response);",
"}",
"message Request {}",
"message Response {}"
].join("\n")).root;
root.resolveAll();
var staticTarget = require("../cli/targets/static");
staticTarget(root, {
create: true,
decode: true,
encode: true,
convert: true,
service: true,
comments: true,
root: "rpcMetadata"
}, function(err, jsCode) {
test.error(err, "static code generation worked");
delete protobuf.roots.rpcMetadata;
var $protobuf = protobuf;
eval(jsCode);
var TestService = protobuf.roots.rpcMetadata.rpcmeta.TestService;
var unary = TestService.prototype.unary;
var serverStream = TestService.prototype.serverStream;
var clientStream = TestService.prototype.clientStream;
test.equal(unary.name, "Unary", "unary method exposes its proto name");
test.equal(unary.path, "/rpcmeta.TestService/Unary", "unary method exposes its rpc path");
test.equal(unary.requestType, "Request", "unary method exposes request type");
test.equal(unary.responseType, "Response", "unary method exposes response type");
test.equal(unary.requestStream, undefined, "unary method has no request stream flag");
test.equal(unary.responseStream, undefined, "unary method has no response stream flag");
test.equal(serverStream.responseStream, true, "server streaming method exposes response stream flag");
test.equal(clientStream.requestStream, true, "client streaming method exposes request stream flag");
require("../cli/pbts").process(jsCode, [], function(dtsErr, tsCode) {
test.error(dtsErr, "declaration generation worked");
test.ok(tsCode.indexOf("unary: rpcmeta.TestService.Unary;") >= 0, "declaration exposes method as typed property");
test.ok(tsCode.indexOf("type Unary = {") >= 0, "declaration emits method callable type");
test.ok(tsCode.indexOf("readonly path: \"/rpcmeta.TestService/Unary\";") >= 0, "declaration exposes literal rpc path");
test.ok(tsCode.indexOf("readonly responseStream: true;") >= 0, "declaration exposes streaming metadata");
delete protobuf.roots.rpcMetadata;
test.end();
});
});
});
});
tape.test("pbjs static toJSON handles runtime-significant field names", function(test) {
cliTest(test, function() {
var root = protobuf.parse("syntax = \"proto3\"; message M { string constructor = 1; }").root;
root.resolveAll();
var staticTarget = require("../cli/targets/static");
staticTarget(root, {
decode: true,
encode: true,
convert: true,
root: "staticFieldShadow"
}, function(err, jsCode) {
test.error(err, "static code generation worked");
delete protobuf.roots.staticFieldShadow;
var $protobuf = protobuf;
eval(jsCode);
var M = protobuf.roots.staticFieldShadow.M,
message = M.decode(M.encode({ constructor: "x" }).finish());
test.equal(JSON.stringify(message), "{\"constructor\":\"x\"}", "should stringify a message with an own constructor field");
delete protobuf.roots.staticFieldShadow;
test.end();
});
});
});
tape.test("pbjs static services can call runtime-significant method names", function(test) {
cliTest(test, function() {
var root = protobuf.parse("syntax = \"proto3\"; message Req {} message Res {} service S { rpc rpcCall(Req) returns (Res); }").root;
root.resolveAll();
var staticTarget = require("../cli/targets/static");
staticTarget(root, {
decode: true,
encode: true,
convert: true,
service: true,
root: "staticServiceShadow"
}, function(err, jsCode) {
test.error(err, "static code generation worked");
delete protobuf.roots.staticServiceShadow;
var $protobuf = protobuf;
eval(jsCode);
var S = protobuf.roots.staticServiceShadow.S,
Res = protobuf.roots.staticServiceShadow.Res,
service = new S(function(method, request, callback) {
callback(null, Res.encode({}).finish());
});
service.rpcCall({}, function(callErr, response) {
test.error(callErr, "should call method named rpcCall");
test.ok(response instanceof Res, "should decode the response");
delete protobuf.roots.staticServiceShadow;
test.end();
});
});
});
});
tape.test("pbjs static code aliases globals shadowed by type names", function(test) {
cliTest(test, function() {
var root = protobuf.parse([
"syntax = \"proto3\";",
"message Object { string value = 1; }",
"message Number { double value = 1; }",
"message Array { repeated string values = 1; }",
"message String { string value = 1; }",
"message Boolean { bool value = 1; }",
"message TypeError { Object nested = 1; }",
"message BigInt { int64 value = 1; }",
"message undefined { string value = 1; }"
].join("\n")).root;
root.resolveAll();
var staticTarget = require("../cli/targets/static");
staticTarget(root, {
create: true,
decode: true,
encode: true,
verify: true,
convert: true,
typeurl: true,
root: "staticGlobalShadow"
}, function(err, jsCode) {
test.error(err, "static code generation worked");
if (err) {
test.end();
return;
}
test.ok(jsCode.indexOf("$Object = $util.global.Object") >= 0, "emits Object alias");
test.ok(jsCode.indexOf("$Number = $util.global.Number") >= 0, "emits Number alias");
test.ok(jsCode.indexOf("$Array = $util.global.Array") >= 0, "emits Array alias");
test.ok(jsCode.indexOf("$undefined = $util.global.undefined") >= 0, "emits undefined alias");
delete protobuf.roots.staticGlobalShadow;
var $protobuf = protobuf;
eval(jsCode);
var root = protobuf.roots.staticGlobalShadow,
ObjectMessage = root.Object,
NumberMessage = root.Number,
ArrayMessage = root.Array,
StringMessage = root.String,
BooleanMessage = root.Boolean,
TypeErrorMessage = root.TypeError,
BigIntMessage = root.BigInt,
UndefinedMessage = root.undefined;
test.equal(ObjectMessage.create({ value: "x" }).value, "x", "creates message named Object");
test.equal(ObjectMessage.verify({ value: "x" }), null, "verifies message named Object");
test.equal(NumberMessage.fromObject({ value: "1.5" }).value, 1.5, "converts message named Number");
test.same(ArrayMessage.fromObject({ values: ["a"] }).values, ["a"], "converts message named Array");
test.equal(StringMessage.fromObject({ value: 1 }).value, "1", "converts message named String");
test.equal(BooleanMessage.fromObject({ value: 1 }).value, true, "converts message named Boolean");
test.throws(function() {
TypeErrorMessage.fromObject({ nested: "x" });
}, TypeError, "throws TypeError when message named TypeError");
test.equal(typeof BigIntMessage.toObject(BigIntMessage.fromObject({ value: 1 }), { longs: globalThis.BigInt }).value, "bigint", "converts message named BigInt");
test.equal(UndefinedMessage.getTypeUrl(), "type.googleapis.com/undefined", "gets type url when message is named undefined");
delete protobuf.roots.staticGlobalShadow;
test.end();
});
});
});
tape.test("pbjs generates correct ES module static-module imports", function(test) {
cliTest(test, function() {
var root = protobuf.loadSync("tests/data/cli/test.proto");
root.resolveAll();
var staticModuleTarget = require("../cli/targets/static-module");
staticModuleTarget(root, {
wrap: "esm",
}, function(err, jsCode) {
test.error(err, "static-module code generation worked");
test.ok(jsCode.includes("import $protobuf from \"protobufjs/minimal.js\";"), "esm wrapper uses a default import and explicit .js extension");
test.end();
});
});
});
tape.test("pbjs resolves imports from cwd by default", function(test) {
cliTest(test, function() {
var output = child_process.execFileSync(process.execPath, [
path.resolve("cli", "bin", "pbjs"),
"-t", "json",
path.join("proto", "a.proto"),
path.join("proto", "b.proto")
], {
cwd: path.resolve("tests", "data", "cli", "default-path"),
encoding: "utf8"
});
var json = JSON.parse(output);
test.ok(json.nested.proto.nested.A, "includes imported type");
test.ok(json.nested.proto.nested.B, "includes importing type");
test.end();
});
});
tape.test("pbjs emits file overview comments on one line", function(test) {
cliTest(test, function() {
var root = new protobuf.Root();
root.comment = "Generated file.";
var staticTarget = require("../cli/targets/static");
staticTarget(root, {
comments: true
}, function(err, jsCode) {
test.error(err, "static code generation worked");
test.ok(jsCode.indexOf(" * @fileoverview Generated file.") >= 0, "emits file overview comment");
test.equal(jsCode.indexOf("@\n * f\n * i\n * l\n * e"), -1, "does not split file overview into characters");
test.end();
});
});
});
tape.test("pbjs keeps es6 as an ES module wrapper alias", function(test) {
cliTest(test, function() {
var root = protobuf.loadSync("tests/data/cli/test.proto");
root.resolveAll();
var staticModuleTarget = require("../cli/targets/static-module");
staticModuleTarget(root, {
wrap: "es6",
}, function(err, jsCode) {
test.error(err, "static-module code generation worked");
test.ok(jsCode.includes("import $protobuf from \"protobufjs/minimal.js\";"), "es6 wrapper alias uses ESM imports");
test.end();
});
});
});
tape.test("pbjs escapes wrapper lint comments", function(test) {
cliTest(test, function() {
var root = new protobuf.Root();
var staticModuleTarget = require("../cli/targets/static-module");
staticModuleTarget(root, {
wrap: "commonjs",
lint: "note */ text"
}, function(err, jsCode) {
test.error(err, "static-module code generation worked");
test.equal(jsCode.indexOf("note */ text"), -1, "does not emit raw comment terminator");
test.ok(jsCode.indexOf("note * / text") >= 0, "escapes comment terminator");
test.doesNotThrow(function() {
new Function("require", "module", "exports", jsCode); // eslint-disable-line no-new-func
}, "should generate parseable output");
test.end();
});
});
});
tape.test("pbjs supports dictionary generated root names", function(test) {
cliTest(test, function() {
var staticTarget = require("../cli/targets/static");
var jsonModuleTarget = require("../cli/targets/json-module");
var root = protobuf.Root.fromJSON({
nested: {
M: {
fields: {}
}
}
});
root.options = Object.create(null);
root.options.__proto__ = { marker: true };
root.options.constructor = { marker: true };
test.equal(Object.getPrototypeOf(protobuf.roots), null, "roots uses dictionary semantics");
delete protobuf.roots.__proto__;
delete protobuf.roots.constructor;
staticTarget(root, {
root: "__proto__"
}, function(staticErr, staticCode) {
test.error(staticErr, "static target accepts dictionary root name");
test.ok(staticCode.indexOf("$protobuf.roots[\"__proto__\"]") >= 0, "static target uses bracket root access");
var $protobuf = protobuf;
test.doesNotThrow(function() {
eval(staticCode);
}, "static output should execute");
test.ok(Object.prototype.hasOwnProperty.call(protobuf.roots, "__proto__"), "static target creates own root property");
jsonModuleTarget(root, {
wrap: "commonjs",
root: "constructor",
lint: ""
}, function(jsonErr, jsonCode) {
test.error(jsonErr, "json-module target accepts dictionary root name");
test.ok(jsonCode.indexOf("$protobuf.roots[\"constructor\"]") >= 0, "json-module target uses bracket root access");
test.ok(jsonCode.indexOf("\"__proto__\":") >= 0, "json-module keeps __proto__ option quoted");
test.ok(jsonCode.indexOf("\"constructor\":") >= 0, "json-module keeps constructor option quoted");
test.equal(jsonCode.indexOf("__proto__:"), -1, "json-module does not emit __proto__ object literal syntax");
var module = { exports: {} };
function localRequire(request) {
if (request.indexOf("protobufjs") === 0)
return protobuf;
throw Error("unexpected require: " + request);
}
test.doesNotThrow(function() {
new Function("require", "module", "exports", jsonCode)(localRequire, module, module.exports); // eslint-disable-line no-new-func
}, "json-module output should execute");
test.ok(Object.prototype.hasOwnProperty.call(protobuf.roots, "constructor"), "json-module target creates own root property");
test.equal(module.exports, protobuf.roots.constructor, "json-module exports dictionary root");
delete protobuf.roots.__proto__;
delete protobuf.roots.constructor;
test.end();
});
});
});
});
tape.test("pbjs supports custom target paths", function(test) {
cliTest(test, function() {
var pbjs = require("../cli/pbjs");
var tmpDir = path.join(".tmp", "pbjs-custom-target-" + process.pid + "-" + Date.now());
var targetPath = path.join(tmpDir, "custom-target.js");
var outPath = path.join(tmpDir, "out.js");
fs.mkdirSync(tmpDir, { recursive: true });
fs.writeFileSync(targetPath, [
"\"use strict\";",
"module.exports = function(root, options, callback) {",
" callback(null, \"custom target:\" + options.target);",
"};"
].join("\n"));
pbjs.main([
"--target", targetPath,
"--out", outPath,
"tests/data/package.proto"
], function(err) {
test.error(err, "custom target generation worked");
test.equal(fs.readFileSync(outPath, "utf8"), "custom target:" + targetPath, "uses required custom target");
fs.unlinkSync(outPath);
fs.unlinkSync(targetPath);
fs.rmdirSync(tmpDir);
test.end();
});
});
});
tape.test("pbjs json-module exports reflection root", function(test) {
cliTest(test, function() {
var root = protobuf.loadSync([
"tests/data/package.proto",
"tests/data/comment_serialization.proto"
]);
root.addJSON({
awesomepackage: {
nested: {
AwesomeMessage: {
fields: {
awesomeField: {
type: "string",
id: 1
}
}
}
}
},
"bad-package": {
nested: {
"Bad-Message": {
fields: {}
}
}
},
lookup: {
nested: {
Collision: {
fields: {}
}
}
},
some: {
nested: {
really: {
nested: {
DeepMessage: {
fields: {}
}
}
}
}
}
});
root.resolveAll();
var jsonModuleTarget = require("../cli/targets/json-module");
jsonModuleTarget(root, {
wrap: "commonjs",
root: "jsonCompat",
es6: true,
lint: "",
service: true,
typeurl: true
}, function(err, jsCode) {
test.error(err, "json-module code generation worked");
delete protobuf.roots.jsonCompat;
var module = { exports: {} };
function localRequire(request) {
if (request.indexOf("protobufjs") === 0)
return protobuf;
throw Error("unexpected require: " + request);
}
test.doesNotThrow(function() {
new Function("require", "module", "exports", jsCode)(localRequire, module, module.exports); // eslint-disable-line no-new-func
}, "generated commonjs module is executable");
test.ok(jsCode.indexOf("require(\"protobufjs/light\")") >= 0, "commonjs wrapper uses light without .js extension");
test.ok(jsCode.indexOf("@type {$protobuf.Root}") >= 0, "documents commonjs root export");
test.ok(jsCode.indexOf("module.exports = $root") >= 0, "keeps commonjs root export");
test.equal(jsCode.indexOf("export const "), -1, "does not emit named exports for commonjs with es6 syntax");
var bundle = module.exports;
test.equal(bundle, protobuf.roots.jsonCompat, "exports the reflection root");
test.equal(typeof bundle.lookup, "function", "keeps root lookup method");
test.equal(bundle.Package, bundle.lookupType("Package"), "exposes upper-case top-level message type");
test.equal(bundle.Package.Repository, bundle.lookupType("Package.Repository"), "exposes upper-case nested message type");
test.equal(bundle.awesomepackage, undefined, "does not add lower-case package aliases to the root");
test.equal(bundle.get("awesomepackage").AwesomeMessage, bundle.lookupType("awesomepackage.AwesomeMessage"), "reflection namespace exposes upper-case package message");
test.equal(bundle.badpackage, undefined, "does not add escaped aliases to the root");
test.equal(bundle.get("bad-package").BadMessage, bundle.lookupType("bad-package.BadMessage"), "reflection namespace exposes escaped package message");
test.equal(bundle.lookup.Collision, undefined, "does not overwrite root methods with lower-case namespace aliases");
test.equal(bundle.get("lookup").Collision, bundle.lookupType("lookup.Collision"), "reflection namespace exposes root method name collision");
test.equal(bundle.some, undefined, "does not add lower-case nested namespace aliases to the root");
test.equal(bundle.get("some").get("really").DeepMessage, bundle.lookupType("some.really.DeepMessage"), "reflection namespaces expose upper-case nested messages");
test.equal(bundle.Package.getTypeUrl(), "type.googleapis.com/Package", "exports top-level getTypeUrl");
test.equal(bundle.Package.Repository.getTypeUrl("example.com"), "example.com/Package.Repository", "exports nested getTypeUrl");
test.equal(bundle.get("awesomepackage").AwesomeMessage.getTypeUrl(), "type.googleapis.com/awesomepackage.AwesomeMessage", "exports package message getTypeUrl");
test.equal(bundle.TestEnum.VALUE, 1, "exports enum values");
test.equal(bundle.TestService, bundle.lookupService("TestService"), "exports service type");
test.equal(typeof bundle.TestService.create(function() {}).testMethod, "function", "service create returns RPC service");
var encoded = bundle.Package.encode(bundle.Package.create({
repository: {
type: "git",
url: "https://example.com/repo.git"
}
})).finish();
test.equal(bundle.Package.decode(encoded).repository.url, "https://example.com/repo.git", "exported types use reflection encode/decode");
var service = bundle.TestService.create(function(method, requestData, callback) {
test.equal(method.name, "testMethod", "service passes reflected method");
test.equal(bundle.TestMessage.decode(requestData).testField, "ping", "service encodes request");
callback(null, bundle.TestMessage.encode({ testField: "pong" }).finish());
});
service.testMethod({ testField: "ping" }, function(rpcErr, response) {
test.error(rpcErr, "service call succeeds");
test.equal(response.testField, "pong", "service decodes response");
delete protobuf.roots.jsonCompat;
test.end();
});
});
});
});
tape.test("pbjs json-module emits ES module named exports", function(test) {
cliTest(test, function() {
var root = protobuf.loadSync([
"tests/data/package.proto",
"tests/data/comment_serialization.proto"
]);
root.addJSON({
awesomepackage: {
nested: {
AwesomeMessage: {
fields: {
awesomeField: {
type: "string",
id: 1
}
}
}
}
},
"bad-package": {
nested: {
"Bad-Message": {
fields: {}
}
}
},
lookup: {
nested: {
Collision: {
fields: {}
}
}
},
some: {
nested: {
really: {
nested: {
DeepMessage: {
fields: {}
}
}
}
}
},
"bad-name": {
nested: {}
},
badname: {
nested: {}
}
});
root.resolveAll();
var jsonModuleTarget = require("../cli/targets/json-module");
jsonModuleTarget(root, {
wrap: "esm",
es6: true,
root: "jsonCompatEsm",
lint: "",
service: true,
typeurl: true
}, function(err, jsCode) {
test.error(err, "json-module code generation worked");
test.ok(jsCode.indexOf("export const Package = $root.get(\"Package\");") >= 0, "exports top-level messages");
test.ok(jsCode.indexOf("export const awesomepackage = $root.get(\"awesomepackage\");") >= 0, "exports lower-case package namespaces");
test.ok(jsCode.indexOf("export const badpackage = $root.get(\"bad-package\");") >= 0, "exports escaped package namespaces");
test.ok(jsCode.indexOf("export const lookup = $root.get(\"lookup\");") >= 0, "exports root method name collisions");
test.ok(jsCode.indexOf("export const some = $root.get(\"some\");") >= 0, "exports top-level lower-case namespace");
test.ok(jsCode.indexOf("export const badname = $root.get(\"bad-name\");") >= 0, "exports first escaped name");
test.equal(jsCode.indexOf("export const badname = $root.get(\"badname\");"), -1, "skips duplicate escaped export names");
test.equal(jsCode.indexOf("$root.awesomepackage ="), -1, "does not mutate the reflection root for lower-case aliases");
test.equal(jsCode.indexOf("Object.create("), -1, "does not generate facade objects");
test.equal(jsCode.indexOf("getTypeUrl = function getTypeUrl"), -1, "uses Type#getTypeUrl instead of per-type generated helpers");
test.ok(jsCode.indexOf("export const TestEnum = $root.get(\"TestEnum\").values;") >= 0, "exports top-level enums");
test.ok(jsCode.indexOf("export const TestService = $root.get(\"TestService\");") >= 0, "exports top-level services");
test.ok(jsCode.indexOf("@type {$protobuf.Root}") >= 0, "documents default root export");
test.ok(jsCode.indexOf("$root as default") >= 0, "keeps default root export");
test.end();
});
});
});
tape.test("pbjs --dts writes module declarations", function(test) {
cliTest(test, function() {
var pbjs = require("../cli/pbjs");
var prefix = path.join(".tmp", "pbjs-dts-test-" + process.pid + "-" + Date.now());
var staticOut = prefix + "-static.js";
var jsonOut = prefix + "-json.js";
var jsonEsmOut = prefix + "-json-esm.js";
var jsonMinimalOut = prefix + "-json-minimal.js";
var staticDts = prefix + "-static.d.ts";
var jsonDts = prefix + "-json.d.ts";
var jsonEsmDts = prefix + "-json-esm.d.ts";
var jsonMinimalDts = prefix + "-json-minimal.d.ts";
if (!fs.existsSync(".tmp"))
fs.mkdirSync(".tmp");
function cleanup() {
[ staticOut, jsonOut, jsonEsmOut, jsonMinimalOut, staticDts, jsonDts, jsonEsmDts, jsonMinimalDts ].forEach(function(file) {
try {
fs.unlinkSync(file);
} catch (e) {
// best effort cleanup
}
});
}
cleanup();
pbjs.main([
"--target", "static-module",
"--wrap", "commonjs",
"--out", staticOut,
"--dts",
"tests/data/package.proto"
], function(staticErr) {
test.error(staticErr, "static-module --dts generation worked");
test.ok(fs.existsSync(staticOut), "writes static-module javascript");
test.ok(fs.existsSync(staticDts), "writes static-module declarations");
var staticTypes = fs.readFileSync(staticDts, "utf8");
test.ok(staticTypes.indexOf("import * as $protobuf from \"protobufjs/minimal\";") >= 0, "uses the static-module dependency in declarations");
test.ok(staticTypes.indexOf("constructor(properties?: Package.$Properties);") >= 0, "keeps constructable static declarations");
test.ok(staticTypes.indexOf("type $Shape = Package.$Properties;") >= 0, "aliases shape to properties when there is no narrowing");
pbjs.main([
"--target", "json-module",
"--wrap", "commonjs",
"--out", jsonOut,
"--dts",
"tests/data/package.proto"
], function(jsonErr) {
test.error(jsonErr, "json-module --dts generation worked");
test.ok(fs.existsSync(jsonOut), "writes json-module javascript");
test.ok(fs.existsSync(jsonDts), "writes json-module declarations");
var jsonTypes = fs.readFileSync(jsonDts, "utf8");
test.ok(jsonTypes.indexOf("import * as $protobuf from \"protobufjs/light\";") >= 0, "uses the json-module dependency in declarations");
test.ok(jsonTypes.indexOf("private constructor();") >= 0, "marks reflection-backed declarations as non-constructable");
test.ok(jsonTypes.indexOf("Reflection-backed declarations are not constructable. Use Package.create(...) instead.") >= 0, "explains create usage");
test.ok(jsonTypes.indexOf("static create(properties?: Package.$Properties): Package;") >= 0, "keeps reflection Type create declaration");
test.equal(jsonTypes.indexOf("constructor(properties?: Package.$Properties);"), -1, "does not expose public message constructors");
pbjs.main([
"--target", "json-module",
"--wrap", "esm",
"--out", jsonEsmOut,
"--dts",
"tests/data/package.proto"
], function(jsonEsmErr) {
test.error(jsonEsmErr, "json-module esm --dts generation worked");
test.ok(fs.existsSync(jsonEsmOut), "writes json-module esm javascript");
test.ok(fs.existsSync(jsonEsmDts), "writes json-module esm declarations");
var jsonEsmTypes = fs.readFileSync(jsonEsmDts, "utf8");
test.ok(jsonEsmTypes.indexOf("import * as $protobuf from \"protobufjs/light.js\";") >= 0, "uses the ESM dependency in declarations");
test.ok(jsonEsmTypes.indexOf("declare const _default: $protobuf.Root;") >= 0, "declares the default root export");
test.ok(jsonEsmTypes.indexOf("export default _default;") >= 0, "exports the default root declaration");
test.ok(jsonEsmTypes.indexOf("export class Package") >= 0, "emits message declarations");
test.ok(jsonEsmTypes.indexOf("class Repository") >= 0, "emits nested message declarations");
pbjs.main([
"--target", "json-module",
"--wrap", "commonjs",
"--out", jsonMinimalOut,
"--dts",
"--dependency", "custom-protobuf",
"--no-create",
"--no-encode",
"--no-decode",
"--no-verify",
"--no-convert",
"--no-delimited",
"--no-typeurl",
"tests/data/package.proto"
], function(jsonMinimalErr) {
test.error(jsonMinimalErr, "json-module --dts respects static method flags");
test.ok(fs.existsSync(jsonMinimalOut), "writes minimal json-module javascript");
test.ok(fs.existsSync(jsonMinimalDts), "writes minimal json-module declarations");
var minimalCode = fs.readFileSync(jsonMinimalOut, "utf8");
var minimalTypes = fs.readFileSync(jsonMinimalDts, "utf8");
test.ok(minimalCode.indexOf("require(\"custom-protobuf\")") >= 0, "uses a custom dependency in javascript");
test.ok(minimalTypes.indexOf("import * as $protobuf from \"custom-protobuf\";") >= 0, "uses a custom dependency in declarations");
test.equal(minimalTypes.indexOf("static create("), -1, "omits disabled create declaration");
test.equal(minimalTypes.indexOf("static encode("), -1, "omits disabled encode declaration");
test.equal(minimalTypes.indexOf("static decode("), -1, "omits disabled decode declaration");
test.equal(minimalTypes.indexOf("static verify("), -1, "omits disabled verify declaration");
test.equal(minimalTypes.indexOf("static fromObject("), -1, "omits disabled fromObject declaration");
test.equal(minimalTypes.indexOf("static toObject("), -1, "omits disabled toObject declaration");
test.equal(minimalTypes.indexOf("static getTypeUrl("), -1, "omits disabled getTypeUrl declaration");
cleanup();
test.end();
});
});
});
});
});
});
tape.test("pbjs --dts narrows oneof interfaces", function(test) {
cliTest(test, function() {
var pbjs = require("../cli/pbjs");
var root = protobuf.loadSync("tests/data/cli/test.proto").resolveAll();
pbjs.generate(root, {
target: "static-module",
wrap: "commonjs",
dts: true
}, function(err, staticCode, staticTypes) {
test.error(err, "static-module --dts generation worked");
test.ok(staticCode.indexOf("@type {{") >= 0, "documents create overloads with TypeScript JSDoc");
test.equal(staticCode.indexOf("@tstype"), -1, "uses standard JSDoc return types");
test.ok(staticTypes.indexOf("export interface IOneofContainer extends OneofContainer.$Properties") >= 0, "keeps a legacy properties interface");
test.ok(staticTypes.indexOf("export class OneofContainer {") >= 0, "does not implement the scoped properties interface");
test.equal(staticTypes.indexOf("implements OneofContainer.$Properties"), -1, "omits explicit scoped properties implementation");
test.ok(staticTypes.indexOf("constructor(properties?: OneofContainer.$Properties);") >= 0, "uses the scoped properties type for construction");
test.ok(staticTypes.indexOf("static create(properties: OneofContainer.$Shape): OneofContainer & OneofContainer.$Shape;") >= 0, "narrows create from oneof-safe input");
test.ok(staticTypes.indexOf("static create(properties?: OneofContainer.$Properties): OneofContainer;") >= 0, "keeps broad create overload");
test.ok(staticTypes.indexOf("static encode(message: OneofContainer.$Properties, writer?: $protobuf.Writer): $protobuf.Writer;") >= 0, "uses the scoped properties type for encoding");
test.ok(staticTypes.indexOf("static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): OneofContainer & OneofContainer.$Shape;") >= 0, "narrows decoded oneof messages");
test.equal(staticTypes.indexOf("$Oneofs"), -1, "does not expose an intermediate oneof type");
test.ok(staticTypes.indexOf(" type $Shape = {\n stringInOneof?: string|null;\n messageInOneof?: Message.$Shape|null;") >= 0, "emits multiline recursive shape fields");
test.ok(staticTypes.indexOf("{ someOneof?: \"messageInOneof\"; stringInOneof?: null; messageInOneof: Message.$Shape }") >= 0, "emits oneof refinement union");
test.ok(staticTypes.indexOf("type $Shape = Message.$Properties;") >= 0, "aliases plain shape type for non-oneof message");
var packagedRoot = protobuf.parse([
"syntax = \"proto3\";",
"package pkg;",
"message OneofContainer {",
" oneof choice {",
" string name = 1;",
" int32 count = 2;",
" }",
"}"
].join("\n")).root.resolveAll();
pbjs.generate(packagedRoot, {
target: "static-module",
wrap: "commonjs",
dts: true
}, function(packagedErr, packagedCode, packagedTypes) {
test.error(packagedErr, "packaged static-module --dts generation worked");
test.ok(packagedCode.indexOf("pkg.OneofContainer") >= 0, "generates packaged static module");
test.ok(packagedTypes.indexOf(" type $Shape = {\n name?: string|null;\n count?: number|null;") >= 0, "indents multiline shape fields in namespaces");
test.ok(packagedTypes.indexOf(" } & (\n ({ choice?: undefined; name?: null; count?: null }") >= 0, "indents multiline shape unions in namespaces");
test.end();
});
});
});
});
tape.test("pbjs --dts generated message typings compile", function(test) {
cliTest(test, function() {
var pbjs = require("../cli/pbjs");
var prefix = path.join(".tmp", "pbjs-dts-types-test-" + process.pid + "-" + Date.now());
var staticOut = prefix + ".js";
var staticDts = prefix + ".d.ts";
var tsOut = prefix + ".check.ts";
var tsconfigOut = prefix + ".tsconfig.json";
var tsc = require.resolve("typescript/bin/tsc");
if (!fs.existsSync(".tmp"))
fs.mkdirSync(".tmp");
function cleanup() {
[ staticOut, staticDts, tsOut, tsconfigOut ].forEach(function(file) {
try {
fs.unlinkSync(file);
} catch (e) {
// best effort cleanup
}
});
}
cleanup();
pbjs.main([
"--target", "static-module",
"--wrap", "commonjs",
"--out", staticOut,
"--dts",
"tests/data/cli/test.proto"
], function(err) {
test.error(err, "static-module --dts generation worked");
fs.writeFileSync(tsOut, [
"import { Enum, Message, OneofContainer } from \"./" + path.basename(prefix) + "\";",
"",
"function expectType<T>(value: T): void { void value; }",
"",
"const message = new Message({ value: 1 });",
"Message.encode(message).finish();",
"Message.encode({ value: 1 }).finish();",
"expectType<number>(Message.decode(new Uint8Array()).value);",
"",
"const byCase = OneofContainer.create({ someOneof: \"stringInOneof\", stringInOneof: \"abc\" });",
"if (byCase.someOneof === \"stringInOneof\") {",
" expectType<string>(byCase.stringInOneof);",
" expectType<null|undefined>(byCase.messageInOneof);",
"}",
"",
"const byField = OneofContainer.create({ stringInOneof: \"abc\" });",
"if (byField.stringInOneof != null) {",
" expectType<string>(byField.stringInOneof);",
" expectType<null|undefined>(byField.messageInOneof);",
"}",
"",
"const byMessage = OneofContainer.create({ messageInOneof: { value: 1 } });",
"if (byMessage.someOneof === \"messageInOneof\")",
" expectType<Message.$Shape>(byMessage.messageInOneof);",
"",
"const broadProperties: OneofContainer.$Properties = {",
" stringInOneof: \"abc\",",
" messageInOneof: { value: 1 }",
"};",
"const broad = OneofContainer.create(broadProperties);",
"expectType<string|null|undefined>(broad.stringInOneof);",
"",
"const constructed = new OneofContainer({ stringInOneof: \"abc\" });",
"expectType<string|null|undefined>(constructed.stringInOneof);",
"",
"const decoded = OneofContainer.decode(new Uint8Array());",
"if (decoded.someOneof === \"stringInOneof\")",
" expectType<string>(decoded.stringInOneof);",
"if (decoded.messageInOneof != null)",
" expectType<Message.$Shape>(decoded.messageInOneof);",
"",
"OneofContainer.encode({",
" regularField: \"regular\",",
" enumField: Enum.SOMETHING,",
" stringInOneof: \"abc\"",
"}).finish();"
].join("\n"));
fs.writeFileSync(tsconfigOut, JSON.stringify({
compilerOptions: {
baseUrl: "..",
esModuleInterop: true,
lib: [ "es2015" ],
noEmit: true,
paths: {
"protobufjs/minimal": [ "minimal" ],
"protobufjs": [ "index" ]
},
strictNullChecks: true,
types: [ "node" ]
},
files: [ path.basename(tsOut) ]
}, null, 4));
child_process.execFile(process.execPath, [ tsc, "-p", tsconfigOut ], function(tscErr, stdout, stderr) {
if (stdout)
process.stdout.write(stdout);
if (stderr)
process.stderr.write(stderr);
test.error(tscErr, "generated declarations type-check");
cleanup();
test.end();
});
});
});
});
tape.test("pbjs escapes static target names", function(test) {
cliTest(test, function() {
var root = protobuf.Root.fromJSON({
nested: {
"1-ns": {
nested: {}
}
}
});
var staticTarget = require("../cli/targets/static");
staticTarget(root, {}, function(err, jsCode) {
test.error(err, "static code generation worked");
test.doesNotThrow(function() {
new Function("$protobuf", jsCode); // eslint-disable-line no-new-func
}, "should generate parseable output");
test.end();
});
});
});
tape.test("pbjs rejects static target escaped name collisions", function(test) {
cliTest(test, function() {
var root = protobuf.Root.fromJSON({
nested: {
"pkg-name": {
nested: {}
},
pkgname: {
nested: {}
}
}
});
var staticTarget = require("../cli/targets/static");
staticTarget(root, {}, function(err) {
test.match(err && err.message, /duplicate generated name 'pkgname'/, "rejects ambiguous generated names");
test.end();
});
});
});
tape.test("pbjs builds static references from escaped path segments", function(test) {
cliTest(test, function() {
var root = protobuf.Root.fromJSON({
nested: {
"pkg.name": {
nested: {
Child: {
fields: {
value: { type: "string", id: 1 }
}
},
Kind: {
values: {
UNKNOWN: 0,
READY: 1
}
},
Parent: {
fields: {
child: { type: "Child", id: 1 },
kind: { type: "Kind", id: 2 }
}
},
TestService: {
methods: {
Call: {
requestType: "Child",
responseType: "Parent"
}
}
}
}
}
}
});
var staticTarget = require("../cli/targets/static");
staticTarget(root, {
create: true,
service: true,
verify: true,
convert: true
}, function(err, jsCode) {
test.error(err, "static code generation worked");
test.equal(jsCode.indexOf("$root.pkg.name"), -1, "does not split dotted descriptor names");
test.ok(jsCode.indexOf("$root.pkgname.Child") >= 0, "uses escaped path segment for request type");
test.ok(jsCode.indexOf("$root.pkgname.Parent") >= 0, "uses escaped path segment for response type");
test.doesNotThrow(function() {
new Function("$protobuf", jsCode); // eslint-disable-line no-new-func
}, "should generate parseable output");
test.end();
});
});
});
tape.test("pbjs sparsify removes extension fields through their declarations", function(test) {
var root = new protobuf.Root({ noGoogleTypes: true }),
ns = root.define("sparse"),
extendedType = new protobuf.Type("ExtendedType"),
declaringType = new protobuf.Type("DeclaringType"),
declaringField = new protobuf.Field("extension", 100, "string", "optional", "ExtendedType"),
mainType = new protobuf.Type("Main");
extendedType.extensions = [[100, 199]];
extendedType.filename = declaringType.filename = declaringField.filename = "imported.proto";
mainType.filename = "main.proto";
declaringType.add(declaringField);
ns.add(extendedType).add(declaringType).add(mainType);
test.ok(declaringField.extensionField, "creates a synthetic extension field");
test.doesNotThrow(function() {
require("../cli/pbjs").sparsify(root, ["main.proto"]);
}, "sparsifies extension fields without removing their synthetic fields directly");
test.equal(root.lookup("sparse.Main"), mainType, "keeps referenced main types");
test.equal(root.lookup("sparse.ExtendedType"), null, "removes unreferenced extended types");
test.equal(root.lookup("sparse.DeclaringType"), null, "removes unreferenced declaring types");
test.equal(declaringField.extensionField, null, "unlinks the synthetic extension field");
test.end();
});
tape.test("pbjs generates static code with message filter", function (test) {
cliTest(test, function () {
var root = protobuf.loadSync("tests/data/cli/test-filter.proto");
root.resolveAll();
var staticTarget = require("../cli/targets/static");
var util = require("../cli/util");
const needMessageConfig = JSON.parse(fs.readFileSync("tests/data/cli/filter.json"));
util.filterMessage(root, needMessageConfig);
staticTarget(root, {
create: true,
decode: true,
encode: true,
convert: true,
"null-defaults": true,
}, function (err, jsCode) {
test.error(err, 'static code generation worked');
// jsCode is the generated code; we'll eval it
// (since this is what we normally does with the code, right?)
// This is a test code. Do not use this in production.
var $protobuf = protobuf;
eval(jsCode);
var NeedMessage1 = protobuf.roots.default.filtertest.NeedMessage1;
var NeedMessage2 = protobuf.roots.default.filtertest.NeedMessage2;
var DependentMessage1 = protobuf.roots.default.filtertest.DependentMessage1;
var DependentMessageFromImport = protobuf.roots.default.DependentMessageFromImport;
var NotNeedMessageInRootFile = protobuf.roots.default.filtertest.NotNeedMessageInRootFile;
var NotNeedMessageInImportFile = protobuf.roots.default.NotNeedMessageInImportFile;
test.ok(NeedMessage1, "NeedMessage1 is loaded");
test.ok(NeedMessage2, "NeedMessage2 is loaded");
test.ok(DependentMessage1, "DependentMessage1 is loaded");
test.ok(DependentMessageFromImport, "DependentMessageFromImport is loaded");
test.notOk(NotNeedMessageInImportFile, "NotNeedMessageInImportFile is not loaded");
test.notOk(NotNeedMessageInRootFile, "NotNeedMessageInRootFile is not loaded");
test.end();
});
});
});