Skip to content

Commit

Permalink
Add more tests for serialization errors (#1051)
Browse files Browse the repository at this point in the history
  • Loading branch information
timostamm authored Jan 7, 2025
1 parent 1164be1 commit 42e834b
Show file tree
Hide file tree
Showing 2 changed files with 275 additions and 52 deletions.
53 changes: 1 addition & 52 deletions packages/protobuf-test/src/json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,7 @@ describe("JsonWriteOptions", () => {
// Coverage for JSON parse errors to guard against regressions.
// We do not cover all cases here. Map fields and oneofs are incomplete,
// and bytes, string, and other scalar types are not tested.
// For serialization errors, see serialization-errors.test.ts
describe("JSON parse errors", () => {
test("fromJsonString() with invalid JSON", () => {
expect(() => fromJsonString(TestAllTypesProto3Schema, "}")).toThrow(
Expand Down Expand Up @@ -1100,58 +1101,6 @@ describe("JSON parse errors", () => {
}
});

describe("JSON serialize errors", () => {
const desc = proto3_ts.Proto3MessageSchema;
test("enum field not a number throws", () => {
const msg = create(desc);
// @ts-expect-error TS2322
msg.singularEnumField = "abc";
expect(() => toJson(desc, msg)).toThrow(
/^cannot encode enum spec.Proto3Enum to JSON: expected number, got "abc"$/,
);
});
test("INT32 field not a number throws", () => {
const msg = create(desc);
// @ts-expect-error TS2322
msg.singularInt32Field = "abc";
expect(() => toJson(desc, msg)).toThrow(
/^cannot encode field spec.Proto3Message.singular_int32_field to JSON: expected number \(int32\), got "abc"$/,
);
});
test("STRING field not a number throws", () => {
const msg = create(desc);
// @ts-expect-error TS2322
msg.singularStringField = 123;
expect(() => toJson(desc, msg)).toThrow(
/^cannot encode field spec.Proto3Message.singular_string_field to JSON: expected string, got 123$/,
);
});
test("BOOL field not a boolean throws", () => {
const msg = create(desc);
// @ts-expect-error TS2322
msg.optionalBoolField = "abc";
expect(() => toJson(desc, msg)).toThrow(
/^cannot encode field spec.Proto3Message.optional_bool_field to JSON: expected boolean, got "abc"$/,
);
});
test("INT64 field not a bigint throws", () => {
const msg = create(desc);
// @ts-expect-error TS2322
msg.singularInt64Field = true;
expect(() => toJson(desc, msg)).toThrow(
/^cannot encode field spec.Proto3Message.singular_int64_field to JSON: expected bigint \(int64\), got true$/,
);
});
test("BYTES field not a Uint8Array throws", () => {
const msg = create(desc);
// @ts-expect-error TS2322
msg.singularBytesField = true;
expect(() => toJson(desc, msg)).toThrow(
/^cannot encode field spec.Proto3Message.singular_bytes_field to JSON: expected Uint8Array, got true$/,
);
});
});

function testJson<Desc extends DescMessage>(
desc: Desc,
init: MessageInitShape<Desc>,
Expand Down
274 changes: 274 additions & 0 deletions packages/protobuf-test/src/serialization-errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
// Copyright 2021-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { describe, expect, test } from "@jest/globals";
import { create, toBinary, toJson } from "@bufbuild/protobuf";
import { FieldMaskSchema, ValueSchema } from "@bufbuild/protobuf/wkt";
import * as proto3_ts from "./gen/ts/extra/proto3_pb.js";
import * as proto2_ts from "./gen/ts/extra/proto2_pb.js";
import * as scalar_ts from "./gen/ts/extra/msg-scalar_pb.js";

describe("serialization errors", () => {
describe("google.protobuf.FieldMask ", () => {
describe("lowerCamelCase path name irreversible", () => {
const fieldMask = create(FieldMaskSchema, {
paths: ["user.displayName", "photo"],
});
test("toJson", () => {
expect(() => {
toJson(FieldMaskSchema, fieldMask);
}).toThrow(
'cannot encode message google.protobuf.FieldMask to JSON: lowerCamelCase of path name "user.displayName" is irreversible',
);
});
test("toBinary", () => {
expect(() => {
toBinary(FieldMaskSchema, fieldMask);
}).not.toThrow();
});
});
});
describe("google.protobuf.Value", () => {
describe("unset value", () => {
// Absence of any variant indicates an error.
// See struct.proto
const value = create(ValueSchema);
test("toJson", () => {
expect(() => toJson(ValueSchema, value)).toThrowError(
"google.protobuf.Value must have a value",
);
});
test("toBinary", () => {
expect(() => toBinary(ValueSchema, value)).not.toThrowError();
});
});
describe("numberValue NaN", () => {
const value = create(ValueSchema, {
kind: { case: "numberValue", value: NaN },
});
test("toJson", () => {
expect(() => toJson(ValueSchema, value)).toThrowError(
"google.protobuf.Value cannot be NaN or Infinity",
);
});
test("toBinary", () => {
expect(() => toBinary(ValueSchema, value)).not.toThrowError();
});
});
describe("numberValue Infinity", () => {
const value = create(ValueSchema, {
kind: { case: "numberValue", value: Infinity },
});
test("toJson", () => {
expect(() => toJson(ValueSchema, value)).toThrowError(
"google.protobuf.Value cannot be NaN or Infinity",
);
});
test("toBinary", () => {
expect(() => toBinary(ValueSchema, value)).not.toThrowError();
});
});
describe("numberValue -Infinity", () => {
const value = create(ValueSchema, {
kind: { case: "numberValue", value: -Infinity },
});
test("toJson", () => {
expect(() => toJson(ValueSchema, value)).toThrowError(
"google.protobuf.Value cannot be NaN or Infinity",
);
});
test("toBinary", () => {
expect(() => toBinary(ValueSchema, value)).not.toThrowError();
});
});
});
describe("enum field", () => {
const desc = proto3_ts.Proto3MessageSchema;
const msg = create(desc);
// @ts-expect-error TS2322
msg.singularEnumField = "abc";
test("toJson", () => {
expect(() => toJson(desc, msg)).toThrow(
/^cannot encode enum spec.Proto3Enum to JSON: expected number, got "abc"$/,
);
});
test("toBinary", () => {
expect(() => toBinary(desc, msg)).toThrow(/^invalid int32: NaN$/);
});
});
describe("repeated enum field", () => {
const desc = proto3_ts.Proto3MessageSchema;
const msg = create(desc);
// @ts-expect-error TS2322
msg.repeatedEnumField = ["abc"];
test("toJson", () => {
expect(() => toJson(desc, msg)).toThrow(
/^cannot encode enum spec.Proto3Enum to JSON: expected number, got "abc"$/,
);
});
test("toBinary", () => {
expect(() => toBinary(desc, msg)).toThrow(/^invalid int32: NaN$/);
});
});
describe("required field", () => {
const desc = proto2_ts.Proto2MessageSchema;
const msg = create(desc);
test("toJson", () => {
expect(() => toJson(desc, msg)).toThrow(
/^cannot encode field spec.Proto2Message.required_string_field to JSON: required field not set$/,
);
});
test("toBinary", () => {
expect(() => toBinary(desc, msg)).toThrow(
/^cannot encode field spec.Proto2Message.required_string_field to binary: required field not set$/,
);
});
});
type ScalarCase = {
name: string;
val(m: scalar_ts.ScalarValuesMessage): void;
jsonErr: RegExp | null;
binaryErr: RegExp | null;
};
describe.each([
{
name: "int32 field string",
val(m) {
// @ts-expect-error TS2322
m.int32Field = "abc";
},
jsonErr:
/^cannot encode field spec.ScalarValuesMessage.int32_field to JSON: expected number \(int32\), got "abc"$/,
binaryErr: /^invalid int32: NaN$/,
},
{
name: "uint32 field -1",
val(m) {
m.uint32Field = -1;
},
jsonErr: null,
binaryErr: /^invalid uint32: -1$/,
},
{
name: "string field 123",
val(m) {
// @ts-expect-error TS2322
m.stringField = 123;
},
jsonErr:
/^cannot encode field spec.ScalarValuesMessage.string_field to JSON: expected string, got 123$/,
binaryErr: null,
},
{
name: `bool field "abc"`,
val(m) {
// @ts-expect-error TS2322
m.boolField = "abc";
},
jsonErr:
/^cannot encode field spec.ScalarValuesMessage.bool_field to JSON: expected boolean, got "abc"/,
binaryErr: null,
},
{
name: "int64 field true",
val(m) {
// @ts-expect-error TS2322
m.int64Field = true;
},
jsonErr:
/^cannot encode field spec.ScalarValuesMessage.int64_field to JSON: expected bigint \(int64\), got true/,
binaryErr: null,
},
{
name: "uint64 field -1",
val(m) {
m.uint64Field = BigInt(-1);
},
jsonErr: null,
binaryErr: /^invalid uint64: -1$/,
},
{
name: "bytes field true",
val(m) {
// @ts-expect-error TS2322
m.bytesField = true;
},
jsonErr:
/^cannot encode field spec.ScalarValuesMessage.bytes_field to JSON: expected Uint8Array, got true/,
binaryErr: /^invalid uint32: undefined$/,
},
] as ScalarCase[])("$name", (kase) => {
const desc = scalar_ts.ScalarValuesMessageSchema;
const msg = create(desc);
kase.val(msg);
test("toJson", () => {
if (kase.jsonErr === null) {
expect(() => toJson(desc, msg)).not.toThrow();
} else {
expect(() => toJson(desc, msg)).toThrow(kase.jsonErr);
}
});
test("toBinary", () => {
if (kase.binaryErr === null) {
expect(() => toBinary(desc, msg)).not.toThrow();
} else {
expect(() => toBinary(desc, msg)).toThrow(kase.binaryErr);
}
});
});
type RepeatedScalarCase = {
name: string;
val(m: scalar_ts.RepeatedScalarValuesMessage): void;
jsonErr: RegExp | null;
binaryErr: RegExp | null;
};
describe.each([
{
name: "repeated uint32 field [-1]",
val(m) {
m.uint32Field = [-1];
},
jsonErr: null,
binaryErr: /^invalid uint32: -1$/,
},
{
name: `repeated int32 field ["abc"]`,
val(m) {
// @ts-expect-error TS2322
m.int32Field = ["abc"];
},
jsonErr:
/^cannot encode field spec.RepeatedScalarValuesMessage.int32_field to JSON: expected ReflectList \(INT32\), got "abc"$/,
binaryErr: /^invalid int32: NaN$/,
},
] as RepeatedScalarCase[])("$name", (kase) => {
const desc = scalar_ts.RepeatedScalarValuesMessageSchema;
const msg = create(desc);
kase.val(msg);
test("toJson", () => {
if (kase.jsonErr === null) {
expect(() => toJson(desc, msg)).not.toThrow();
} else {
expect(() => toJson(desc, msg)).toThrow(kase.jsonErr);
}
});
test("toBinary", () => {
if (kase.binaryErr === null) {
expect(() => toBinary(desc, msg)).not.toThrow();
} else {
expect(() => toBinary(desc, msg)).toThrow(kase.binaryErr);
}
});
});
});

0 comments on commit 42e834b

Please sign in to comment.