Skip to content

Commit 81c05fb

Browse files
authored
Implemented IID Delete API (#41)
* Implemented IID Delete API * Cleaned up code * Updated test case * Updating comments * Improved error handling * Fixing malformed error messages due to extra format placeholders; Using the recommended Go error message format (no caps or punctuation)
1 parent 60b1855 commit 81c05fb

File tree

7 files changed

+414
-0
lines changed

7 files changed

+414
-0
lines changed

firebase.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"cloud.google.com/go/firestore"
2424

2525
"firebase.google.com/go/auth"
26+
"firebase.google.com/go/iid"
2627
"firebase.google.com/go/internal"
2728
"firebase.google.com/go/storage"
2829

@@ -89,6 +90,15 @@ func (a *App) Firestore(ctx context.Context) (*firestore.Client, error) {
8990
return firestore.NewClient(ctx, a.projectID, a.opts...)
9091
}
9192

93+
// InstanceID returns an instance of iid.Client.
94+
func (a *App) InstanceID(ctx context.Context) (*iid.Client, error) {
95+
conf := &internal.InstanceIDConfig{
96+
ProjectID: a.projectID,
97+
Opts: a.opts,
98+
}
99+
return iid.NewClient(ctx, conf)
100+
}
101+
92102
// NewApp creates a new App from the provided config and client options.
93103
//
94104
// If the client options contain a valid credential (a service account file, a refresh token file or an

firebase_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,18 @@ func TestFirestoreWithNoProjectID(t *testing.T) {
281281
}
282282
}
283283

284+
func TestInstanceID(t *testing.T) {
285+
ctx := context.Background()
286+
app, err := NewApp(ctx, nil, option.WithCredentialsFile("testdata/service_account.json"))
287+
if err != nil {
288+
t.Fatal(err)
289+
}
290+
291+
if c, err := app.InstanceID(ctx); c == nil || err != nil {
292+
t.Errorf("InstanceID() = (%v, %v); want (iid, nil)", c, err)
293+
}
294+
}
295+
284296
func TestCustomTokenSource(t *testing.T) {
285297
ctx := context.Background()
286298
ts := &testTokenSource{AccessToken: "mock-token-from-custom"}

iid/iid.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright 2017 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package iid contains functions for deleting instance IDs from Firebase projects.
16+
package iid
17+
18+
import (
19+
"errors"
20+
"fmt"
21+
"net/http"
22+
23+
"google.golang.org/api/transport"
24+
25+
"firebase.google.com/go/internal"
26+
27+
"golang.org/x/net/context"
28+
)
29+
30+
const iidEndpoint = "https://console.firebase.google.com/v1"
31+
32+
var errorCodes = map[int]string{
33+
400: "malformed instance id argument",
34+
401: "request not authorized",
35+
403: "project does not match instance ID or the client does not have sufficient privileges",
36+
404: "failed to find the instance id",
37+
409: "already deleted",
38+
429: "request throttled out by the backend server",
39+
500: "internal server error",
40+
503: "backend servers are over capacity",
41+
}
42+
43+
// Client is the interface for the Firebase Instance ID service.
44+
type Client struct {
45+
// To enable testing against arbitrary endpoints.
46+
endpoint string
47+
client *internal.HTTPClient
48+
project string
49+
}
50+
51+
// NewClient creates a new instance of the Firebase instance ID Client.
52+
//
53+
// This function can only be invoked from within the SDK. Client applications should access the
54+
// the instance ID service through firebase.App.
55+
func NewClient(ctx context.Context, c *internal.InstanceIDConfig) (*Client, error) {
56+
if c.ProjectID == "" {
57+
return nil, errors.New("project id is required to access instance id client")
58+
}
59+
60+
hc, _, err := transport.NewHTTPClient(ctx, c.Opts...)
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
return &Client{
66+
endpoint: iidEndpoint,
67+
client: &internal.HTTPClient{Client: hc},
68+
project: c.ProjectID,
69+
}, nil
70+
}
71+
72+
// DeleteInstanceID deletes an instance ID from Firebase.
73+
//
74+
// This can be used to delete an instance ID and associated user data from a Firebase project,
75+
// pursuant to the General Data protection Regulation (GDPR).
76+
func (c *Client) DeleteInstanceID(ctx context.Context, iid string) error {
77+
if iid == "" {
78+
return errors.New("instance id must not be empty")
79+
}
80+
81+
url := fmt.Sprintf("%s/project/%s/instanceId/%s", c.endpoint, c.project, iid)
82+
resp, err := c.client.Do(ctx, &internal.Request{Method: "DELETE", URL: url})
83+
if err != nil {
84+
return err
85+
}
86+
87+
if msg, ok := errorCodes[resp.Status]; ok {
88+
return fmt.Errorf("instance id %q: %s", iid, msg)
89+
}
90+
return resp.CheckStatus(http.StatusOK)
91+
}

iid/iid_test.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Copyright 2017 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package iid
16+
17+
import (
18+
"fmt"
19+
"net/http"
20+
"net/http/httptest"
21+
"testing"
22+
23+
"google.golang.org/api/option"
24+
25+
"firebase.google.com/go/internal"
26+
27+
"golang.org/x/net/context"
28+
)
29+
30+
var testIIDConfig = &internal.InstanceIDConfig{
31+
ProjectID: "test-project",
32+
Opts: []option.ClientOption{
33+
option.WithTokenSource(&internal.MockTokenSource{AccessToken: "test-token"}),
34+
},
35+
}
36+
37+
func TestNoProjectID(t *testing.T) {
38+
client, err := NewClient(context.Background(), &internal.InstanceIDConfig{})
39+
if client != nil || err == nil {
40+
t.Errorf("NewClient() = (%v, %v); want = (nil, error)", client, err)
41+
}
42+
}
43+
44+
func TestInvalidInstanceID(t *testing.T) {
45+
ctx := context.Background()
46+
client, err := NewClient(ctx, testIIDConfig)
47+
if err != nil {
48+
t.Fatal(err)
49+
}
50+
51+
if err := client.DeleteInstanceID(ctx, ""); err == nil {
52+
t.Errorf("DeleteInstanceID(empty) = nil; want error")
53+
}
54+
}
55+
56+
func TestDeleteInstanceID(t *testing.T) {
57+
var tr *http.Request
58+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
59+
tr = r
60+
w.Header().Set("Content-Type", "application/json")
61+
w.Write([]byte("{}"))
62+
}))
63+
defer ts.Close()
64+
65+
ctx := context.Background()
66+
client, err := NewClient(ctx, testIIDConfig)
67+
if err != nil {
68+
t.Fatal(err)
69+
}
70+
client.endpoint = ts.URL
71+
if err := client.DeleteInstanceID(ctx, "test-iid"); err != nil {
72+
t.Errorf("DeleteInstanceID() = %v; want nil", err)
73+
}
74+
75+
if tr == nil {
76+
t.Fatalf("Request = nil; want non-nil")
77+
}
78+
if tr.Method != "DELETE" {
79+
t.Errorf("Method = %q; want = %q", tr.Method, "DELETE")
80+
}
81+
if tr.URL.Path != "/project/test-project/instanceId/test-iid" {
82+
t.Errorf("Path = %q; want = %q", tr.URL.Path, "/project/test-project/instanceId/test-iid")
83+
}
84+
if h := tr.Header.Get("Authorization"); h != "Bearer test-token" {
85+
t.Errorf("Authorization = %q; want = %q", h, "Bearer test-token")
86+
}
87+
}
88+
89+
func TestDeleteInstanceIDError(t *testing.T) {
90+
status := 200
91+
var tr *http.Request
92+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
93+
tr = r
94+
w.WriteHeader(status)
95+
w.Header().Set("Content-Type", "application/json")
96+
w.Write([]byte("{}"))
97+
}))
98+
defer ts.Close()
99+
100+
ctx := context.Background()
101+
client, err := NewClient(ctx, testIIDConfig)
102+
if err != nil {
103+
t.Fatal(err)
104+
}
105+
client.endpoint = ts.URL
106+
107+
for k, v := range errorCodes {
108+
status = k
109+
err := client.DeleteInstanceID(ctx, "test-iid")
110+
if err == nil {
111+
t.Fatal("DeleteInstanceID() = nil; want = error")
112+
}
113+
114+
want := fmt.Sprintf("instance id %q: %s", "test-iid", v)
115+
if err.Error() != want {
116+
t.Errorf("DeleteInstanceID() = %v; want = %v", err, want)
117+
}
118+
119+
if tr == nil {
120+
t.Fatalf("Request = nil; want non-nil")
121+
}
122+
if tr.Method != "DELETE" {
123+
t.Errorf("Method = %q; want = %q", tr.Method, "DELETE")
124+
}
125+
if tr.URL.Path != "/project/test-project/instanceId/test-iid" {
126+
t.Errorf("Path = %q; want = %q", tr.URL.Path, "/project/test-project/instanceId/test-iid")
127+
}
128+
if h := tr.Header.Get("Authorization"); h != "Bearer test-token" {
129+
t.Errorf("Authorization = %q; want = %q", h, "Bearer test-token")
130+
}
131+
tr = nil
132+
}
133+
}
134+
135+
func TestDeleteInstanceIDUnexpectedError(t *testing.T) {
136+
var tr *http.Request
137+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
138+
tr = r
139+
w.WriteHeader(511)
140+
w.Header().Set("Content-Type", "application/json")
141+
w.Write([]byte("{}"))
142+
}))
143+
defer ts.Close()
144+
145+
ctx := context.Background()
146+
client, err := NewClient(ctx, testIIDConfig)
147+
if err != nil {
148+
t.Fatal(err)
149+
}
150+
client.endpoint = ts.URL
151+
152+
err = client.DeleteInstanceID(ctx, "test-iid")
153+
if err == nil {
154+
t.Fatal("DeleteInstanceID() = nil; want = error")
155+
}
156+
157+
want := "http error status: 511; reason: {}"
158+
if err.Error() != want {
159+
t.Errorf("DeleteInstanceID() = %v; want = %v", err, want)
160+
}
161+
162+
if tr == nil {
163+
t.Fatalf("Request = nil; want non-nil")
164+
}
165+
if tr.Method != "DELETE" {
166+
t.Errorf("Method = %q; want = %q", tr.Method, "DELETE")
167+
}
168+
if tr.URL.Path != "/project/test-project/instanceId/test-iid" {
169+
t.Errorf("Path = %q; want = %q", tr.URL.Path, "/project/test-project/instanceId/test-iid")
170+
}
171+
if h := tr.Header.Get("Authorization"); h != "Bearer test-token" {
172+
t.Errorf("Authorization = %q; want = %q", h, "Bearer test-token")
173+
}
174+
}
175+
176+
func TestDeleteInstanceIDConnectionError(t *testing.T) {
177+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
178+
// Do nothing
179+
}))
180+
ts.Close()
181+
182+
ctx := context.Background()
183+
client, err := NewClient(ctx, testIIDConfig)
184+
if err != nil {
185+
t.Fatal(err)
186+
}
187+
client.endpoint = ts.URL
188+
if err := client.DeleteInstanceID(ctx, "test-iid"); err == nil {
189+
t.Errorf("DeleteInstanceID() = nil; want = error")
190+
return
191+
}
192+
}

integration/iid/iid_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright 2017 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package iid contains integration tests for the firebase.google.com/go/iid package.
16+
package iid
17+
18+
import (
19+
"context"
20+
"flag"
21+
"log"
22+
"os"
23+
"testing"
24+
25+
"firebase.google.com/go/iid"
26+
"firebase.google.com/go/integration/internal"
27+
)
28+
29+
var client *iid.Client
30+
31+
func TestMain(m *testing.M) {
32+
flag.Parse()
33+
if testing.Short() {
34+
log.Println("skipping instance ID integration tests in short mode.")
35+
os.Exit(0)
36+
}
37+
38+
ctx := context.Background()
39+
app, err := internal.NewTestApp(ctx)
40+
if err != nil {
41+
log.Fatalln(err)
42+
}
43+
44+
client, err = app.InstanceID(ctx)
45+
if err != nil {
46+
log.Fatalln(err)
47+
}
48+
49+
os.Exit(m.Run())
50+
}
51+
52+
func TestNonExisting(t *testing.T) {
53+
err := client.DeleteInstanceID(context.Background(), "non-existing")
54+
if err == nil {
55+
t.Errorf("DeleteInstanceID(non-existing) = nil; want error")
56+
}
57+
want := `instance id "non-existing": failed to find the instance id`
58+
if err.Error() != want {
59+
t.Errorf("DeleteInstanceID(non-existing) = %v; want = %v", err, want)
60+
}
61+
}

0 commit comments

Comments
 (0)