From 61f3fab757af15ee240ad5d336ca50cad4d15658 Mon Sep 17 00:00:00 2001 From: Adam Chalmers Date: Tue, 22 Sep 2020 16:28:05 -0500 Subject: [PATCH] TUN-3410: Request the v1 Tunnelstore API --- tunnelstore/client.go | 66 +++++++++++---- tunnelstore/client_test.go | 165 +++++++++++++++++++++++++++++++++---- 2 files changed, 196 insertions(+), 35 deletions(-) diff --git a/tunnelstore/client.go b/tunnelstore/client.go index 545d1752..12582002 100644 --- a/tunnelstore/client.go +++ b/tunnelstore/client.go @@ -27,6 +27,7 @@ var ( ErrUnauthorized = errors.New("unauthorized") ErrBadRequest = errors.New("incorrect request parameters") ErrNotFound = errors.New("not found") + ErrAPINoSuccess = errors.New("API call failed") ) type Tunnel struct { @@ -39,7 +40,7 @@ type Tunnel struct { type Connection struct { ColoName string `json:"colo_name"` - ID uuid.UUID `json:"uuid"` + ID uuid.UUID `json:"id"` IsPendingReconnect bool `json:"is_pending_reconnect"` } @@ -91,11 +92,9 @@ func (dr *DNSRoute) MarshalJSON() ([]byte, error) { func (dr *DNSRoute) UnmarshalResult(body io.Reader) (RouteResult, error) { var result DNSRouteResult - if err := json.NewDecoder(body).Decode(&result); err != nil { - return nil, err - } + err := parseResponse(body, &result) result.route = dr - return &result, nil + return &result, err } func (dr *DNSRoute) RecordType() string { @@ -152,11 +151,9 @@ func (lr *LBRoute) RecordType() string { func (lr *LBRoute) UnmarshalResult(body io.Reader) (RouteResult, error) { var result LBRouteResult - if err := json.NewDecoder(body).Decode(&result); err != nil { - return nil, err - } + err := parseResponse(body, &result) result.route = lr - return &result, nil + return &result, err } func (res *LBRouteResult) SuccessSummary() string { @@ -313,16 +310,18 @@ func (r *RESTClient) ListTunnels(filter *Filter) ([]*Tunnel, error) { defer resp.Body.Close() if resp.StatusCode == http.StatusOK { - var tunnels []*Tunnel - if err := json.NewDecoder(resp.Body).Decode(&tunnels); err != nil { - return nil, errors.Wrap(err, "failed to decode response") - } - return tunnels, nil + return parseListTunnels(resp.Body) } return nil, r.statusCodeToError("list tunnels", resp) } +func parseListTunnels(body io.ReadCloser) ([]*Tunnel, error) { + var tunnels []*Tunnel + err := parseResponse(body, &tunnels) + return tunnels, err +} + func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID) error { endpoint := r.baseEndpoints.accountLevel endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID)) @@ -370,15 +369,48 @@ func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) ( req.Header.Set("Content-Type", jsonContentType) } req.Header.Add("X-Auth-User-Service-Key", r.authToken) + req.Header.Add("Accept", "application/json;version=1") return r.client.Do(req) } +func parseResponse(reader io.Reader, data interface{}) error { + // Schema for Tunnelstore responses in the v1 API. + // Roughly, it's a wrapper around a particular result that adds failures/errors/etc + var result struct { + Result json.RawMessage `json:"result"` + Success bool `json:"success"` + Errors []string `json:"errors"` + } + // First, parse the wrapper and check the API call succeeded + if err := json.NewDecoder(reader).Decode(&result); err != nil { + return errors.Wrap(err, "failed to decode response") + } + if err := checkErrors(result.Errors); err != nil { + return err + } + if !result.Success { + return ErrAPINoSuccess + } + // At this point we know the API call succeeded, so, parse out the inner + // result into the datatype provided as a parameter. + return json.Unmarshal(result.Result, &data) +} + func unmarshalTunnel(reader io.Reader) (*Tunnel, error) { var tunnel Tunnel - if err := json.NewDecoder(reader).Decode(&tunnel); err != nil { - return nil, errors.Wrap(err, "failed to decode response") + err := parseResponse(reader, &tunnel) + return &tunnel, err +} + +func checkErrors(errs []string) error { + if len(errs) == 0 { + return nil } - return &tunnel, nil + if len(errs) == 1 { + return fmt.Errorf("API error: %s", errs[0]) + } + allErrs := strings.Join(errs, "; ") + return fmt.Errorf("API errors: %s", allErrs) } func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error { diff --git a/tunnelstore/client_test.go b/tunnelstore/client_test.go index 61aa5cab..708bc47c 100644 --- a/tunnelstore/client_test.go +++ b/tunnelstore/client_test.go @@ -1,9 +1,16 @@ package tunnelstore import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "reflect" "strings" "testing" + "time" + "github.com/google/uuid" "github.com/stretchr/testify/assert" ) @@ -12,7 +19,7 @@ func TestDNSRouteUnmarshalResult(t *testing.T) { userHostname: "example.com", } - result, err := route.UnmarshalResult(strings.NewReader(`{"cname": "new"}`)) + result, err := route.UnmarshalResult(strings.NewReader(`{"success": true, "result": {"cname": "new"}}`)) assert.NoError(t, err) assert.Equal(t, &DNSRouteResult{ @@ -20,8 +27,19 @@ func TestDNSRouteUnmarshalResult(t *testing.T) { CName: ChangeNew, }, result) - _, err = route.UnmarshalResult(strings.NewReader(`abc`)) - assert.NotNil(t, err) + badJSON := []string{ + `abc`, + `{"success": false, "result": {"cname": "new"}}`, + `{"errors": ["foo"], "result": {"cname": "new"}}`, + `{"errors": ["foo", "bar"], "result": {"cname": "new"}}`, + `{"result": {"cname": "new"}}`, + `{"result": {"cname": "new"}}`, + } + + for _, j := range badJSON { + _, err = route.UnmarshalResult(strings.NewReader(j)) + assert.NotNil(t, err) + } } func TestLBRouteUnmarshalResult(t *testing.T) { @@ -30,7 +48,7 @@ func TestLBRouteUnmarshalResult(t *testing.T) { lbPool: "pool", } - result, err := route.UnmarshalResult(strings.NewReader(`{"pool": "unchanged", "load_balancer": "updated"}`)) + result, err := route.UnmarshalResult(strings.NewReader(`{"success": true, "result": {"pool": "unchanged", "load_balancer": "updated"}}`)) assert.NoError(t, err) assert.Equal(t, &LBRouteResult{ @@ -39,8 +57,18 @@ func TestLBRouteUnmarshalResult(t *testing.T) { Pool: ChangeUnchanged, }, result) - _, err = route.UnmarshalResult(strings.NewReader(`abc`)) - assert.NotNil(t, err) + badJSON := []string{ + `abc`, + `{"success": false, "result": {"pool": "unchanged", "load_balancer": "updated"}}`, + `{"errors": ["foo"], "result": {"pool": "unchanged", "load_balancer": "updated"}}`, + `{"errors": ["foo", "bar"], "result": {"pool": "unchanged", "load_balancer": "updated"}}`, + `{"result": {"pool": "unchanged", "load_balancer": "updated"}}`, + } + + for _, j := range badJSON { + _, err = route.UnmarshalResult(strings.NewReader(j)) + assert.NotNil(t, err) + } } func TestLBRouteResultSuccessSummary(t *testing.T) { @@ -54,25 +82,126 @@ func TestLBRouteResultSuccessSummary(t *testing.T) { pool Change expected string }{ - {ChangeNew, ChangeNew, "Created load balancer lb.example.com and added a new pool POOL with this tunnel as an origin" }, - {ChangeNew, ChangeUpdated, "Created load balancer lb.example.com with an existing pool POOL which was updated to use this tunnel as an origin" }, - {ChangeNew, ChangeUnchanged, "Created load balancer lb.example.com with an existing pool POOL which already has this tunnel as an origin" }, - {ChangeUpdated, ChangeNew, "Added new pool POOL with this tunnel as an origin to load balancer lb.example.com" }, - {ChangeUpdated, ChangeUpdated, "Updated pool POOL to use this tunnel as an origin and added it to load balancer lb.example.com" }, - {ChangeUpdated, ChangeUnchanged, "Added pool POOL, which already has this tunnel as an origin, to load balancer lb.example.com" }, - {ChangeUnchanged, ChangeNew, "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard" }, - {ChangeUnchanged, ChangeUpdated, "Added this tunnel as an origin in pool POOL which is already used by load balancer lb.example.com" }, - {ChangeUnchanged, ChangeUnchanged, "Load balancer lb.example.com already uses pool POOL which has this tunnel as an origin" }, - {"", "", "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard" }, - {"a", "b", "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard" }, + {ChangeNew, ChangeNew, "Created load balancer lb.example.com and added a new pool POOL with this tunnel as an origin"}, + {ChangeNew, ChangeUpdated, "Created load balancer lb.example.com with an existing pool POOL which was updated to use this tunnel as an origin"}, + {ChangeNew, ChangeUnchanged, "Created load balancer lb.example.com with an existing pool POOL which already has this tunnel as an origin"}, + {ChangeUpdated, ChangeNew, "Added new pool POOL with this tunnel as an origin to load balancer lb.example.com"}, + {ChangeUpdated, ChangeUpdated, "Updated pool POOL to use this tunnel as an origin and added it to load balancer lb.example.com"}, + {ChangeUpdated, ChangeUnchanged, "Added pool POOL, which already has this tunnel as an origin, to load balancer lb.example.com"}, + {ChangeUnchanged, ChangeNew, "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard"}, + {ChangeUnchanged, ChangeUpdated, "Added this tunnel as an origin in pool POOL which is already used by load balancer lb.example.com"}, + {ChangeUnchanged, ChangeUnchanged, "Load balancer lb.example.com already uses pool POOL which has this tunnel as an origin"}, + {"", "", "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard"}, + {"a", "b", "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard"}, } for i, tt := range tests { res := &LBRouteResult{ route: route, LoadBalancer: tt.lb, Pool: tt.pool, - } + } actual := res.SuccessSummary() assert.Equal(t, tt.expected, actual, "case %d", i+1) } } + +func Test_parseListTunnels(t *testing.T) { + type args struct { + body string + } + tests := []struct { + name string + args args + want []*Tunnel + wantErr bool + }{ + { + name: "empty list", + args: args{body: `{"success": true, "result": []}`}, + want: []*Tunnel{}, + }, + { + name: "success is false", + args: args{body: `{"success": false, "result": []}`}, + wantErr: true, + }, + { + name: "errors are present", + args: args{body: `{"errors": ["foo"], "result": []}`}, + wantErr: true, + }, + { + name: "invalid response", + args: args{body: `abc`}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + body := ioutil.NopCloser(bytes.NewReader([]byte(tt.args.body))) + got, err := parseListTunnels(body) + if (err != nil) != tt.wantErr { + t.Errorf("parseListTunnels() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseListTunnels() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_unmarshalTunnel(t *testing.T) { + type args struct { + reader io.Reader + } + tests := []struct { + name string + args args + want *Tunnel + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := unmarshalTunnel(tt.args.reader) + if (err != nil) != tt.wantErr { + t.Errorf("unmarshalTunnel() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("unmarshalTunnel() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUnmarshalTunnelOk(t *testing.T) { + + jsonBody := `{"success": true, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}` + expected := Tunnel{ + ID: uuid.Nil, + Name: "test", + CreatedAt: time.Time{}, + Connections: []Connection{}, + } + actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody))) + assert.NoError(t, err) + assert.Equal(t, &expected, actual) +} + +func TestUnmarshalTunnelErr(t *testing.T) { + + tests := []string{ + `abc`, + `{"success": true, "result": abc}`, + `{"success": false, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`, + `{"errors": ["foo"], "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`, + } + + for i, test := range tests { + _, err := unmarshalTunnel(bytes.NewReader([]byte(test))) + assert.Error(t, err, fmt.Sprintf("Test #%v failed", i)) + } +}