Skip to content

Commit 633463e

Browse files
authored
Ensure errors are wrapped in FirestoreError in DatastoreImpl methods (#4788)
1 parent 03e97b8 commit 633463e

File tree

3 files changed

+240
-6
lines changed

3 files changed

+240
-6
lines changed

.changeset/clever-icons-leave.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/firestore': patch
3+
---
4+
5+
Ensure that errors get wrapped in FirestoreError

packages/firestore/src/remote/datastore.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,14 @@ class DatastoreImpl extends Datastore {
9999
);
100100
})
101101
.catch((error: FirestoreError) => {
102-
if (error.code === Code.UNAUTHENTICATED) {
103-
this.credentials.invalidateToken();
102+
if (error.name === 'FirebaseError') {
103+
if (error.code === Code.UNAUTHENTICATED) {
104+
this.credentials.invalidateToken();
105+
}
106+
throw error;
107+
} else {
108+
throw new FirestoreError(Code.UNKNOWN, error.toString());
104109
}
105-
throw error;
106110
});
107111
}
108112

@@ -124,10 +128,14 @@ class DatastoreImpl extends Datastore {
124128
);
125129
})
126130
.catch((error: FirestoreError) => {
127-
if (error.code === Code.UNAUTHENTICATED) {
128-
this.credentials.invalidateToken();
131+
if (error.name === 'FirebaseError') {
132+
if (error.code === Code.UNAUTHENTICATED) {
133+
this.credentials.invalidateToken();
134+
}
135+
throw error;
136+
} else {
137+
throw new FirestoreError(Code.UNKNOWN, error.toString());
129138
}
130-
throw error;
131139
});
132140
}
133141

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
/**
2+
* @license
3+
* Copyright 2021 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect, use } from 'chai';
19+
import * as chaiAsPromised from 'chai-as-promised';
20+
21+
import { EmptyCredentialsProvider, Token } from '../../../src/api/credentials';
22+
import { DatabaseId } from '../../../src/core/database_info';
23+
import { Connection, Stream } from '../../../src/remote/connection';
24+
import {
25+
Datastore,
26+
newDatastore,
27+
invokeCommitRpc,
28+
invokeBatchGetDocumentsRpc
29+
} from '../../../src/remote/datastore';
30+
import { JsonProtoSerializer } from '../../../src/remote/serializer';
31+
import { Code, FirestoreError } from '../../../src/util/error';
32+
33+
use(chaiAsPromised);
34+
35+
// TODO(b/185584343): Improve the coverage of these tests.
36+
// At the time of writing, the tests only cover the error handling in
37+
// `invokeRPC()` and `invokeStreamingRPC()`.
38+
describe('Datastore', () => {
39+
class MockConnection implements Connection {
40+
invokeRPC<Req, Resp>(
41+
rpcName: string,
42+
path: string,
43+
request: Req,
44+
token: Token | null
45+
): Promise<Resp> {
46+
throw new Error('MockConnection.invokeRPC() must be replaced');
47+
}
48+
49+
invokeStreamingRPC<Req, Resp>(
50+
rpcName: string,
51+
path: string,
52+
request: Req,
53+
token: Token | null
54+
): Promise<Resp[]> {
55+
throw new Error('MockConnection.invokeStreamingRPC() must be replaced');
56+
}
57+
58+
openStream<Req, Resp>(
59+
rpcName: string,
60+
token: Token | null
61+
): Stream<Req, Resp> {
62+
throw new Error('MockConnection.openStream() must be replaced');
63+
}
64+
}
65+
66+
class MockCredentialsProvider extends EmptyCredentialsProvider {
67+
invalidateTokenInvoked = false;
68+
invalidateToken(): void {
69+
this.invalidateTokenInvoked = true;
70+
}
71+
}
72+
73+
const serializer = new JsonProtoSerializer(
74+
new DatabaseId('test-project'),
75+
/* useProto3Json= */ false
76+
);
77+
78+
async function invokeDatastoreImplInvokeRpc(
79+
datastore: Datastore
80+
): Promise<void> {
81+
// Since we cannot access the `DatastoreImpl` class directly, invoke its
82+
// `invokeRPC()` method indirectly via `invokeCommitRpc()`.
83+
await invokeCommitRpc(datastore, /* mutations= */ []);
84+
}
85+
86+
async function invokeDatastoreImplInvokeStreamingRPC(
87+
datastore: Datastore
88+
): Promise<void> {
89+
// Since we cannot access the `DatastoreImpl` class directly, invoke its
90+
// `invokeStreamingRPC()` method indirectly via
91+
// `invokeBatchGetDocumentsRpc()`.
92+
await invokeBatchGetDocumentsRpc(datastore, /* keys= */ []);
93+
}
94+
95+
it('newDatastore() returns an an instance of Datastore', () => {
96+
const datastore = newDatastore(
97+
new EmptyCredentialsProvider(),
98+
new MockConnection(),
99+
serializer
100+
);
101+
expect(datastore).to.be.an.instanceof(Datastore);
102+
});
103+
104+
it('DatastoreImpl.invokeRPC() fails if terminated', async () => {
105+
const datastore = newDatastore(
106+
new EmptyCredentialsProvider(),
107+
new MockConnection(),
108+
serializer
109+
);
110+
datastore.terminate();
111+
await expect(invokeDatastoreImplInvokeRpc(datastore))
112+
.to.eventually.be.rejectedWith(/terminated/)
113+
.and.include({
114+
'name': 'FirebaseError',
115+
'code': Code.FAILED_PRECONDITION
116+
});
117+
});
118+
119+
it('DatastoreImpl.invokeRPC() rethrows a FirestoreError', async () => {
120+
const connection = new MockConnection();
121+
connection.invokeRPC = () =>
122+
Promise.reject(new FirestoreError(Code.ABORTED, 'zzyzx'));
123+
const credentials = new MockCredentialsProvider();
124+
const datastore = newDatastore(credentials, connection, serializer);
125+
await expect(invokeDatastoreImplInvokeRpc(datastore))
126+
.to.eventually.be.rejectedWith('zzyzx')
127+
.and.include({
128+
'name': 'FirebaseError',
129+
'code': Code.ABORTED
130+
});
131+
expect(credentials.invalidateTokenInvoked).to.be.false;
132+
});
133+
134+
it('DatastoreImpl.invokeRPC() wraps unknown exceptions in a FirestoreError', async () => {
135+
const connection = new MockConnection();
136+
connection.invokeRPC = () => Promise.reject('zzyzx');
137+
const credentials = new MockCredentialsProvider();
138+
const datastore = newDatastore(credentials, connection, serializer);
139+
await expect(invokeDatastoreImplInvokeRpc(datastore))
140+
.to.eventually.be.rejectedWith('zzyzx')
141+
.and.include({
142+
'name': 'FirebaseError',
143+
'code': Code.UNKNOWN
144+
});
145+
expect(credentials.invalidateTokenInvoked).to.be.false;
146+
});
147+
148+
it('DatastoreImpl.invokeRPC() invalidates the token if unauthenticated', async () => {
149+
const connection = new MockConnection();
150+
connection.invokeRPC = () =>
151+
Promise.reject(new FirestoreError(Code.UNAUTHENTICATED, 'zzyzx'));
152+
const credentials = new MockCredentialsProvider();
153+
const datastore = newDatastore(credentials, connection, serializer);
154+
await expect(invokeDatastoreImplInvokeRpc(datastore))
155+
.to.eventually.be.rejectedWith('zzyzx')
156+
.and.include({
157+
'name': 'FirebaseError',
158+
'code': Code.UNAUTHENTICATED
159+
});
160+
expect(credentials.invalidateTokenInvoked).to.be.true;
161+
});
162+
163+
it('DatastoreImpl.invokeStreamingRPC() fails if terminated', async () => {
164+
const datastore = newDatastore(
165+
new EmptyCredentialsProvider(),
166+
new MockConnection(),
167+
serializer
168+
);
169+
datastore.terminate();
170+
await expect(invokeDatastoreImplInvokeStreamingRPC(datastore))
171+
.to.eventually.be.rejectedWith(/terminated/)
172+
.and.include({
173+
'name': 'FirebaseError',
174+
'code': Code.FAILED_PRECONDITION
175+
});
176+
});
177+
178+
it('DatastoreImpl.invokeStreamingRPC() rethrows a FirestoreError', async () => {
179+
const connection = new MockConnection();
180+
connection.invokeStreamingRPC = () =>
181+
Promise.reject(new FirestoreError(Code.ABORTED, 'zzyzx'));
182+
const credentials = new MockCredentialsProvider();
183+
const datastore = newDatastore(credentials, connection, serializer);
184+
await expect(invokeDatastoreImplInvokeStreamingRPC(datastore))
185+
.to.eventually.be.rejectedWith('zzyzx')
186+
.and.include({
187+
'name': 'FirebaseError',
188+
'code': Code.ABORTED
189+
});
190+
expect(credentials.invalidateTokenInvoked).to.be.false;
191+
});
192+
193+
it('DatastoreImpl.invokeStreamingRPC() wraps unknown exceptions in a FirestoreError', async () => {
194+
const connection = new MockConnection();
195+
connection.invokeStreamingRPC = () => Promise.reject('zzyzx');
196+
const credentials = new MockCredentialsProvider();
197+
const datastore = newDatastore(credentials, connection, serializer);
198+
await expect(invokeDatastoreImplInvokeStreamingRPC(datastore))
199+
.to.eventually.be.rejectedWith('zzyzx')
200+
.and.include({
201+
'name': 'FirebaseError',
202+
'code': Code.UNKNOWN
203+
});
204+
expect(credentials.invalidateTokenInvoked).to.be.false;
205+
});
206+
207+
it('DatastoreImpl.invokeStreamingRPC() invalidates the token if unauthenticated', async () => {
208+
const connection = new MockConnection();
209+
connection.invokeStreamingRPC = () =>
210+
Promise.reject(new FirestoreError(Code.UNAUTHENTICATED, 'zzyzx'));
211+
const credentials = new MockCredentialsProvider();
212+
const datastore = newDatastore(credentials, connection, serializer);
213+
await expect(invokeDatastoreImplInvokeStreamingRPC(datastore))
214+
.to.eventually.be.rejectedWith('zzyzx')
215+
.and.include({
216+
'name': 'FirebaseError',
217+
'code': Code.UNAUTHENTICATED
218+
});
219+
expect(credentials.invalidateTokenInvoked).to.be.true;
220+
});
221+
});

0 commit comments

Comments
 (0)