Skip to content

Commit 93d6eec

Browse files
authored
Add test coverage for protobuf conversion edge cases (#2496)
1 parent 24e632e commit 93d6eec

2 files changed

Lines changed: 135 additions & 3 deletions

File tree

lib/src/firestore-to-proto.spec.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { toMessage } from './firestore-to-proto';
1919
import { Constructor } from 'protobufjs';
2020

2121
const {
22+
AuditInfo,
2223
Coordinates,
2324
Job,
2425
LinearRing,
@@ -28,6 +29,7 @@ const {
2829
Task,
2930
LocationOfInterest,
3031
} = GroundProtos.ground.v1beta1;
32+
const { Timestamp } = GroundProtos.google.protobuf;
3133

3234
describe('toMessage()', () => {
3335
[
@@ -138,6 +140,13 @@ describe('toMessage()', () => {
138140
},
139141
expected: new Survey(),
140142
},
143+
{
144+
desc: 'skips repeated field when value is not an array',
145+
input: {
146+
'1': 'not an array',
147+
},
148+
expected: new LinearRing(),
149+
},
141150
{
142151
desc: 'converts enum value',
143152
input: {
@@ -152,6 +161,45 @@ describe('toMessage()', () => {
152161
input: {},
153162
expected: new Task.DateTimeQuestion(),
154163
},
164+
{
165+
desc: 'skips non-numeric enum value',
166+
input: {
167+
'1': 'not-a-number',
168+
},
169+
expected: new Task.DateTimeQuestion(),
170+
},
171+
{
172+
desc: 'converts google.protobuf.Timestamp field',
173+
input: {
174+
'1': 'user-123',
175+
'2': { '1': 1700000000, '2': 500 },
176+
},
177+
expected: new AuditInfo({
178+
userId: 'user-123',
179+
clientTimestamp: new Timestamp({ seconds: 1700000000, nanos: 500 }),
180+
}),
181+
},
182+
{
183+
desc: 'skips non-numeric keys',
184+
input: {
185+
'2': 'Survey name',
186+
notANumber: 'should be ignored',
187+
foo: 42,
188+
},
189+
expected: new Survey({
190+
name: 'Survey name',
191+
}),
192+
},
193+
{
194+
desc: 'skips unrecognized field numbers',
195+
input: {
196+
'2': 'Survey name',
197+
'999': 'unknown field should be ignored',
198+
},
199+
expected: new Survey({
200+
name: 'Survey name',
201+
}),
202+
},
155203
{
156204
desc: 'converts repeated message',
157205
input: {
@@ -171,8 +219,42 @@ describe('toMessage()', () => {
171219
},
172220
].forEach(({ desc, input, expected }) =>
173221
it(desc, () => {
174-
const output = toMessage(input, expected.constructor as Constructor<any>);
222+
const output = toMessage(
223+
input,
224+
expected.constructor as Constructor<unknown>
225+
);
175226
expect(output).toEqual(expected);
176227
})
177228
);
229+
230+
it('logs and skips fields whose conversion returns an Error', () => {
231+
const debugSpy = spyOn(console, 'debug');
232+
const output = toMessage(
233+
{
234+
'2': 'Survey name',
235+
'4': 'not an object', // acl map expects object → Error
236+
},
237+
Survey
238+
);
239+
expect(output).toEqual(new Survey({ name: 'Survey name' }));
240+
expect(debugSpy).toHaveBeenCalledWith(jasmine.any(Error));
241+
});
242+
243+
it('returns Error when constructor is not a protojs message', () => {
244+
class NotAProtoMessage {}
245+
const output = toMessage({}, NotAProtoMessage as Constructor<unknown>);
246+
expect(output).toEqual(jasmine.any(Error));
247+
expect((output as Error).message).toContain('is not a protojs message');
248+
});
249+
250+
it('returns Error when message type not found in registry', () => {
251+
class UnknownProtoMessage {
252+
static getTypeUrl(): string {
253+
return '/ground.v1beta1.DoesNotExist';
254+
}
255+
}
256+
const output = toMessage({}, UnknownProtoMessage as Constructor<unknown>);
257+
expect(output).toEqual(jasmine.any(Error));
258+
expect((output as Error).message).toContain('not found in registry');
259+
});
178260
});

lib/src/proto-to-firestore.spec.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
/**
22
* Copyright 2024 The Ground Authors.
33
*
4-
* Licensed under the Apache License, Version 2.0 (the 'License');
4+
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
66
* You may obtain a copy of the License at
77
*
88
* https://www.apache.org/licenses/LICENSE-2.0
99
*
1010
* Unless required by applicable law or agreed to in writing, software
11-
* distributed under the License is distributed on an 'AS IS' BASIS,
11+
* distributed under the License is distributed on an "AS IS" BASIS,
1212
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
@@ -146,4 +146,54 @@ describe('toDocumentData()', () => {
146146
expect(output).toEqual(expected);
147147
})
148148
);
149+
150+
it('returns empty object for message with no fields set', () => {
151+
expect(toDocumentData({})).toEqual({});
152+
});
153+
154+
it('logs and drops fields whose value has an unsupported type', () => {
155+
const errorSpy = spyOn(console, 'error');
156+
const survey = new Survey({ name: 'Survey name' });
157+
(survey as any).description = () => 'not a string'; // function is unsupported
158+
const output = toDocumentData(survey);
159+
expect(output).toEqual({
160+
[s.name]: 'Survey name',
161+
[s.state]: Survey.State.STATE_UNSPECIFIED,
162+
[s.generalAccess]: Survey.GeneralAccess.GENERAL_ACCESS_UNSPECIFIED,
163+
[s.dataVisibility]: Survey.DataVisibility.DATA_VISIBILITY_UNSPECIFIED,
164+
});
165+
expect(errorSpy).toHaveBeenCalledWith(jasmine.any(Error));
166+
});
167+
168+
it('drops fields whose value is empty string', () => {
169+
const output = toDocumentData(
170+
new Survey({ name: '', description: 'desc' })
171+
);
172+
expect(output).toEqual({
173+
[s.description]: 'desc',
174+
[s.state]: Survey.State.STATE_UNSPECIFIED,
175+
[s.generalAccess]: Survey.GeneralAccess.GENERAL_ACCESS_UNSPECIFIED,
176+
[s.dataVisibility]: Survey.DataVisibility.DATA_VISIBILITY_UNSPECIFIED,
177+
});
178+
});
179+
180+
it('recursively converts array input', () => {
181+
const output = toDocumentData([
182+
new Coordinates({ latitude: 1, longitude: 2 }),
183+
new Coordinates({ latitude: 3, longitude: 4 }),
184+
]);
185+
expect(output).toEqual([
186+
{ [c.latitude]: 1, [c.longitude]: 2 },
187+
{ [c.latitude]: 3, [c.longitude]: 4 },
188+
]);
189+
});
190+
191+
it('throws when message type is not registered', () => {
192+
class UnknownMessage {
193+
foo = 'bar';
194+
}
195+
expect(() => toDocumentData(new UnknownMessage())).toThrowError(
196+
/Unknown message type UnknownMessage/
197+
);
198+
});
149199
});

0 commit comments

Comments
 (0)