...

Source file src/cloud.google.com/go/storage/hmac_test.go

Documentation: cloud.google.com/go/storage

     1  // Copyright 2019 Google LLC
     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 storage
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"net/http"
    21  	"net/url"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	"cloud.google.com/go/internal/testutil"
    27  	"github.com/google/go-cmp/cmp/cmpopts"
    28  	"google.golang.org/api/googleapi"
    29  	"google.golang.org/api/iterator"
    30  )
    31  
    32  func TestHMACKeyHandle_GetParsing(t *testing.T) {
    33  	mt := &mockTransport{}
    34  	client := mockClient(t, mt)
    35  	projectID := "hmackey-project-id"
    36  	ctx := context.Background()
    37  
    38  	tests := []struct {
    39  		res     string
    40  		want    *HMACKey
    41  		wantErr string
    42  	}{
    43  		{
    44  			res: fmt.Sprintf(`
    45                              {
    46                                  "kind": "storage#hmacKeyMetadata",
    47                                  "projectId":%q,"state":"ACTIVE",
    48                                  "timeCreated": "2019-07-06T11:21:58+00:00",
    49                                  "updated": "2019-07-06T11:22:18+00:00"
    50                              }`, projectID),
    51  			want: &HMACKey{
    52  				State:       Active,
    53  				ProjectID:   projectID,
    54  				UpdatedTime: time.Date(2019, 07, 06, 11, 22, 18, 0, time.UTC),
    55  				CreatedTime: time.Date(2019, 07, 06, 11, 21, 58, 0, time.UTC),
    56  			},
    57  		},
    58  		{
    59  			res: fmt.Sprintf(`
    60                              {
    61                                  "kind": "storage#hmacKeyMetadata",
    62                                  "projectId":%q,"state":"ACTIVE",
    63                                  "timeCreated": "2019-07-06T11:21:58+00:00",
    64                                  "updated": "2019-07-06T11:22:18+00:00"
    65                              }`, projectID),
    66  			want: &HMACKey{
    67  				State:       Active,
    68  				ProjectID:   projectID,
    69  				UpdatedTime: time.Date(2019, 07, 06, 11, 22, 18, 0, time.UTC),
    70  				CreatedTime: time.Date(2019, 07, 06, 11, 21, 58, 0, time.UTC),
    71  			},
    72  		},
    73  		{
    74  			res: `{}`,
    75  			// CreatedTime must be formatted in RFC 3339.
    76  			wantErr: `CreatedTime: parsing time "" as "2006-01-02T15:04:05Z07:00"`,
    77  		},
    78  		{
    79  			res: `{"timeCreated": "2019-07-foo"}`,
    80  			// CreatedTime must be formatted in RFC 3339.
    81  			wantErr: `CreatedTime: parsing time "2019-07-foo" as "2006-01-02T15:04:05Z07:00"`,
    82  		},
    83  		{
    84  			res: `{
    85                                  "kind": "storage#hmacKeyMetadata",
    86                                  "state":"INACTIVE",
    87                                  "timeCreated": "2019-07-06T11:21:58+00:00"
    88                              }`,
    89  			// UpdatedTime must be formatted in RFC 3339.
    90  			wantErr: `UpdatedTime: parsing time "" as "2006-01-02T15:04:05Z07:00"`,
    91  		},
    92  	}
    93  
    94  	for i, tt := range tests {
    95  		mt.addResult(&http.Response{
    96  			ProtoMajor:    1,
    97  			ProtoMinor:    1,
    98  			ContentLength: int64(len(tt.res)),
    99  			Status:        "OK",
   100  			StatusCode:    200,
   101  			Body:          bodyReader(tt.res),
   102  		}, nil)
   103  		hkh := client.HMACKeyHandle(projectID, "some-access-key-id")
   104  		got, err := hkh.Get(ctx)
   105  		if tt.wantErr != "" {
   106  			if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
   107  				t.Errorf("#%d: failed to match errors:\ngot:  %q\nwant: %q", i, err, tt.wantErr)
   108  			}
   109  			if got != nil {
   110  				t.Errorf("#%d: unexpectedly got a non-nil result: %#v\n", i, got)
   111  			}
   112  			continue
   113  		}
   114  
   115  		if err != nil {
   116  			t.Errorf("#%d: got an unexpected error: %v", i, err)
   117  			continue
   118  		}
   119  
   120  		if diff := testutil.Diff(got, tt.want); diff != "" {
   121  			t.Errorf("#%d: got - want +\n\n%s", i, diff)
   122  		}
   123  	}
   124  }
   125  
   126  func TestHMACKeyHandle_Get_NotFound(t *testing.T) {
   127  	mt := &mockTransport{}
   128  	client := mockClient(t, mt)
   129  	ctx := context.Background()
   130  
   131  	mt.addResult(&http.Response{
   132  		ProtoMajor: 2,
   133  		ProtoMinor: 0,
   134  		Status:     "OK",
   135  		StatusCode: http.StatusNotFound,
   136  		Body:       bodyReader("Access ID not found in project"),
   137  	}, nil)
   138  
   139  	hkh := client.HMACKeyHandle("project-id", "some-access-key-id")
   140  	_, gotErr := hkh.Get(ctx)
   141  
   142  	wantErr := &googleapi.Error{
   143  		Body:    "Access ID not found in project",
   144  		Code:    http.StatusNotFound,
   145  		Message: "",
   146  	}
   147  	if diff := testutil.Diff(gotErr, wantErr, cmpopts.IgnoreUnexported(googleapi.Error{})); diff != "" {
   148  		t.Fatalf("Error mismatch, got - want +\n%s", diff)
   149  	}
   150  }
   151  
   152  func TestHMACKeyHandle_Delete(t *testing.T) {
   153  	mt := &mockTransport{}
   154  	client := mockClient(t, mt)
   155  	ctx := context.Background()
   156  
   157  	tests := []struct {
   158  		statusCode int
   159  		msg        string
   160  		wantErr    error
   161  	}{
   162  		{
   163  			statusCode: http.StatusBadRequest,
   164  			msg:        "Cannot delete keys in 'ACTIVE' state",
   165  			wantErr: &googleapi.Error{
   166  				Code: http.StatusBadRequest, Message: "Cannot delete keys in 'ACTIVE' state",
   167  				Body: `{"error":{"message":"Cannot delete keys in 'ACTIVE' state"}}`,
   168  			},
   169  		},
   170  		{
   171  			statusCode: http.StatusNotFound,
   172  			msg:        "random message",
   173  			wantErr: &googleapi.Error{
   174  				Code: http.StatusNotFound, Message: "random message",
   175  				Body: `{"error":{"message":"random message"}}`,
   176  			},
   177  		},
   178  		{
   179  			statusCode: http.StatusNotFound,
   180  			msg:        "Access ID not found in project",
   181  			wantErr: &googleapi.Error{
   182  				Code: http.StatusNotFound, Message: "Access ID not found in project",
   183  				Body: `{"error":{"message":"Access ID not found in project"}}`,
   184  			},
   185  		},
   186  	}
   187  
   188  	for i, tt := range tests {
   189  		mt.addResult(&http.Response{
   190  			ProtoMajor: 2,
   191  			ProtoMinor: 0,
   192  			Status:     tt.msg,
   193  			StatusCode: tt.statusCode,
   194  			Body:       bodyReader(fmt.Sprintf(`{"error":{"message":%q}}`, tt.msg)),
   195  		}, nil)
   196  
   197  		hkh := client.HMACKeyHandle("project", "access-key-id")
   198  		err := hkh.Delete(ctx)
   199  
   200  		if diff := testutil.Diff(err, tt.wantErr, cmpopts.IgnoreUnexported(googleapi.Error{})); diff != "" {
   201  			t.Errorf("#%d: error mismatch got - want +\n%s", i, diff)
   202  		}
   203  	}
   204  }
   205  
   206  func TestHMACKeyHandle_Create(t *testing.T) {
   207  	mt := &mockTransport{}
   208  	client := mockClient(t, mt)
   209  	projectID := "hmackey-project-id"
   210  	serviceAccountEmail := "service-account-email-1"
   211  	ctx := context.Background()
   212  
   213  	tests := []struct {
   214  		res     string
   215  		want    *HMACKey
   216  		wantErr string
   217  	}{
   218  		{
   219  			res: `
   220                              {
   221                                  "kind": "storage#hmackey",
   222  				"secret":"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
   223                                  "metadata": {
   224                                      "projectId":"project-id","state":"ACTIVE",
   225                                      "timeCreated": "2019-07-06T11:21:58+00:00",
   226                                      "updated": "2019-07-06T11:22:18+00:00"
   227                                  }
   228                              }`,
   229  			want: &HMACKey{
   230  				Secret:      "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
   231  				State:       Active,
   232  				ProjectID:   "project-id",
   233  				UpdatedTime: time.Date(2019, 07, 06, 11, 22, 18, 0, time.UTC),
   234  				CreatedTime: time.Date(2019, 07, 06, 11, 21, 58, 0, time.UTC),
   235  			},
   236  		},
   237  		{
   238  			res:     `{}`,
   239  			wantErr: "Metadata cannot be nil",
   240  		},
   241  		{
   242  			res: `{"metadata":{}}`,
   243  			// CreatedTime must be non-empty and it must formatted in RFC 3339.
   244  			wantErr: `CreatedTime: parsing time "" as "2006-01-02T15:04:05Z07:00"`,
   245  		},
   246  		{
   247  			res: `{"metadata":{"timeCreated": "2019-07-foo"}}`,
   248  			// CreatedTime must be formatted in RFC 3339.
   249  			wantErr: `CreatedTime: parsing time "2019-07-foo" as "2006-01-02T15:04:05Z07:00"`,
   250  		},
   251  		{
   252  			res: `{
   253                                  "kind": "storage#hmackey",
   254  				"secret":"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
   255                                  "metadata":{
   256                                      "kind": "storage#hmacKeyMetadata",
   257                                      "state":"ACTIVE",
   258                                      "timeCreated": "2019-07-06T12:11:33+00:00",
   259                                      "projectId": "project-id",
   260                                      "updated": ""
   261                                  }
   262                              }`,
   263  			// ONLY during creation is it okay for UpdatedTime to not be set.
   264  			want: &HMACKey{
   265  				Secret:      "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
   266  				State:       Active,
   267  				ProjectID:   "project-id",
   268  				CreatedTime: time.Date(2019, 07, 06, 12, 11, 33, 0, time.UTC),
   269  			},
   270  		},
   271  	}
   272  
   273  	for i, tt := range tests {
   274  		mt.addResult(&http.Response{
   275  			ProtoMajor:    1,
   276  			ProtoMinor:    1,
   277  			ContentLength: int64(len(tt.res)),
   278  			Status:        "OK",
   279  			StatusCode:    200,
   280  			Body:          bodyReader(tt.res),
   281  		}, nil)
   282  		got, err := client.CreateHMACKey(ctx, projectID, serviceAccountEmail)
   283  		if tt.wantErr != "" {
   284  			if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
   285  				t.Errorf("#%d: failed to match errors:\ngot:  %q\nwant: %q", i, err, tt.wantErr)
   286  			}
   287  			if got != nil {
   288  				t.Errorf("#%d: unexpectedly got a non-nil result: %#v\n", i, got)
   289  			}
   290  			continue
   291  		}
   292  
   293  		if err != nil {
   294  			t.Errorf("#%d: got an unexpected error: %v", i, err)
   295  			continue
   296  		}
   297  
   298  		if diff := testutil.Diff(got, tt.want); diff != "" {
   299  			t.Errorf("#%d: got - want +\n\n%s", i, diff)
   300  		}
   301  	}
   302  
   303  	// Lastly ensure that a blank service account will return an error.
   304  	mt.addResult(&http.Response{
   305  		ProtoMajor: 1,
   306  		ProtoMinor: 1,
   307  		Status:     "OK",
   308  		StatusCode: 200,
   309  		Body:       bodyReader("{}"),
   310  	}, nil)
   311  	hk, err := client.CreateHMACKey(ctx, projectID, "")
   312  	if err == nil {
   313  		t.Fatal("Unexpectedly succeeded in creating a key using a blank service account email")
   314  	}
   315  	if !strings.Contains(err.Error(), "non-blank service account email") {
   316  		t.Fatalf("Expected an error about a non-blank service account email: %v", err)
   317  	}
   318  	if hk != nil {
   319  		t.Fatalf("Unexpectedly got back a created HMACKey: %#v", hk)
   320  	}
   321  }
   322  
   323  func TestHMACKey_UpdateState(t *testing.T) {
   324  	// This test ensures that updating the state can only
   325  	// happen with either of Active or Inactive.
   326  
   327  	mt := &mockTransport{}
   328  	client := mockClient(t, mt)
   329  	projectID := "hmackey-project-id"
   330  	ctx := context.Background()
   331  
   332  	hkh := client.HMACKeyHandle(projectID, "some-access-id")
   333  
   334  	// 1. Ensure that invalid states are NOT accepted for an Update.
   335  	invalidStates := []HMACState{"", Deleted, "active", "inactive", "foo_bar"}
   336  	for _, invalidState := range invalidStates {
   337  		t.Run("invalid-"+string(invalidState), func(t *testing.T) {
   338  			_, err := hkh.Update(ctx, HMACKeyAttrsToUpdate{
   339  				State: invalidState,
   340  			})
   341  			if err == nil {
   342  				t.Fatal("Unexpectedly succeeded")
   343  			}
   344  			invalidStateMsg := fmt.Sprintf(`storage: invalid state %q for update, must be either "ACTIVE" or "INACTIVE"`, invalidState)
   345  			if err.Error() != invalidStateMsg {
   346  				t.Fatalf("Mismatched error: got:  %q\nwant: %q", err, invalidStateMsg)
   347  			}
   348  		})
   349  	}
   350  
   351  	// 2. Ensure that valid states for Update are accepted.
   352  	validStates := []HMACState{Active, Inactive}
   353  	for _, validState := range validStates {
   354  		t.Run("valid-"+string(validState), func(t *testing.T) {
   355  			resBody := fmt.Sprintf(`{
   356                                      "kind": "storage#hmacKeyMetadata",
   357                                      "state":%q,
   358                                      "timeCreated": "2019-07-11T12:11:33+00:00",
   359                                      "projectId": "project-id",
   360                                      "updated": "2019-07-11T12:13:33+00:00"
   361                              }`, validState)
   362  			mt.addResult(&http.Response{
   363  				ProtoMajor:    1,
   364  				ProtoMinor:    1,
   365  				ContentLength: int64(len(resBody)),
   366  				Status:        "OK",
   367  				StatusCode:    200,
   368  				Body:          bodyReader(resBody),
   369  			}, nil)
   370  
   371  			hu, err := hkh.Update(ctx, HMACKeyAttrsToUpdate{
   372  				State: validState,
   373  			})
   374  			if err != nil {
   375  				t.Fatalf("Unexpected failure: %v", err)
   376  			}
   377  			if hu.State != validState {
   378  				t.Fatalf("Unexpected updated state %q, expected %q", hu.State, validState)
   379  			}
   380  		})
   381  	}
   382  }
   383  
   384  func TestHMACKey_ListFull(t *testing.T) {
   385  	mt := &mockTransport{}
   386  	client := mockClient(t, mt)
   387  	projectID := "hmackey-project-id"
   388  	ctx := context.Background()
   389  
   390  	maxPages := 2
   391  	page := 0
   392  	mockResponse := func() {
   393  		defer func() {
   394  			page++
   395  		}()
   396  
   397  		var body string
   398  		if page >= maxPages {
   399  			body = `{"kind":"storage#hmacKeysMetadata","items":[]}`
   400  		} else {
   401  			offset := page * 2
   402  			body = fmt.Sprintf(`
   403                          {
   404                              "kind": "storage#hmacKeysMetadata",
   405                              "items": [{
   406                                  "accessId": "accessid-%d",
   407                                  "timeCreated": "2019-08-05T12:11:10+00:00",
   408                                  "state": "ACTIVE"
   409                              }, {
   410                                  "accessId": "accessid-%d",
   411                                  "timeCreated": "2019-08-05T13:12:11+00:00",
   412                                  "state": "INACTIVE"
   413                              }],
   414                              "nextPageToken": "pageToken"
   415                          }`, offset+1, offset+2)
   416  		}
   417  
   418  		mt.addResult(&http.Response{
   419  			ProtoMajor: 2,
   420  			ProtoMinor: 0,
   421  			Status:     "OK",
   422  			StatusCode: 200,
   423  			Body:       bodyReader(body),
   424  		}, nil)
   425  	}
   426  
   427  	iter := client.ListHMACKeys(ctx, projectID)
   428  
   429  	var gotKeys []*HMACKey
   430  	for {
   431  		mockResponse()
   432  		key, err := iter.Next()
   433  		if err == iterator.Done {
   434  			break
   435  		}
   436  		if err != nil {
   437  			t.Fatalf("Unexpected error: %v", err)
   438  		}
   439  		gotKeys = append(gotKeys, key)
   440  	}
   441  
   442  	wantKeys := []*HMACKey{
   443  		{
   444  			AccessID:    "accessid-1",
   445  			CreatedTime: time.Date(2019, time.August, 5, 12, 11, 10, 0, time.UTC),
   446  			State:       Active,
   447  		},
   448  		{
   449  			AccessID:    "accessid-2",
   450  			CreatedTime: time.Date(2019, time.August, 5, 13, 12, 11, 0, time.UTC),
   451  			State:       Inactive,
   452  		},
   453  		{
   454  			AccessID:    "accessid-3",
   455  			CreatedTime: time.Date(2019, time.August, 5, 12, 11, 10, 0, time.UTC),
   456  			State:       Active,
   457  		},
   458  		{
   459  			AccessID:    "accessid-4",
   460  			CreatedTime: time.Date(2019, time.August, 5, 13, 12, 11, 0, time.UTC),
   461  			State:       Inactive,
   462  		},
   463  	}
   464  
   465  	if diff := testutil.Diff(gotKeys, wantKeys); diff != "" {
   466  		t.Fatalf("Response mismatch: got - want +\n%s", diff)
   467  	}
   468  }
   469  
   470  func TestHMACKey_List_Options(t *testing.T) {
   471  	mt := &mockTransport{}
   472  	client := mockClient(t, mt)
   473  	projectID := "hmackey-project-id"
   474  
   475  	// Our goal is just to examine the issued HTTP request's URL's
   476  	// Path and Query to ensure that we have the appropriate paramters.
   477  	tests := []struct {
   478  		name      string
   479  		opts      []HMACKeyOption
   480  		wantQuery url.Values
   481  	}{
   482  		{
   483  			name: "defaults",
   484  			wantQuery: url.Values{
   485  				"alt":         {"json"},
   486  				"prettyPrint": {"false"},
   487  			},
   488  		},
   489  		{
   490  			name:      "show deleted keys",
   491  			opts:      []HMACKeyOption{ShowDeletedHMACKeys()},
   492  			wantQuery: url.Values{"alt": {"json"}, "prettyPrint": {"false"}, "showDeletedKeys": {"true"}},
   493  		},
   494  		{
   495  			name: "for service account",
   496  			opts: []HMACKeyOption{ForHMACKeyServiceAccountEmail("foo@example.org")},
   497  			wantQuery: url.Values{
   498  				"alt":                 {"json"},
   499  				"prettyPrint":         {"false"},
   500  				"serviceAccountEmail": {"foo@example.org"},
   501  			},
   502  		},
   503  		{
   504  			name: "for userProjectID",
   505  			opts: []HMACKeyOption{UserProjectForHMACKeys("project-x")},
   506  			wantQuery: url.Values{
   507  				"alt":         {"json"},
   508  				"prettyPrint": {"false"},
   509  				"userProject": {"project-x"},
   510  			},
   511  		},
   512  		{
   513  			name: "all options",
   514  			opts: []HMACKeyOption{
   515  				ForHMACKeyServiceAccountEmail("foo@example.org"),
   516  				UserProjectForHMACKeys("project-x"),
   517  				ShowDeletedHMACKeys(),
   518  			},
   519  			wantQuery: url.Values{
   520  				"alt":                 {"json"},
   521  				"prettyPrint":         {"false"},
   522  				"serviceAccountEmail": {"foo@example.org"},
   523  				"showDeletedKeys":     {"true"},
   524  				"userProject":         {"project-x"},
   525  			},
   526  		},
   527  	}
   528  
   529  	for _, tt := range tests {
   530  		t.Run(tt.name, func(t *testing.T) {
   531  			body := `{"kind":"storage#hmacKeysMetadata","items":[]}`
   532  			mt.addResult(&http.Response{
   533  				ProtoMajor:    2,
   534  				ProtoMinor:    0,
   535  				ContentLength: int64(len(body)),
   536  				Status:        "OK",
   537  				StatusCode:    200,
   538  				Body:          bodyReader(body),
   539  			}, nil)
   540  			ctx, cancel := context.WithCancel(context.Background())
   541  			iter := client.ListHMACKeys(ctx, projectID, tt.opts...)
   542  			_, _ = iter.Next()
   543  			cancel()
   544  
   545  			gotQuery := mt.gotReq.URL.Query()
   546  			if diff := testutil.Diff(gotQuery, tt.wantQuery); diff != "" {
   547  				t.Errorf("Query mismatch: got - want +\n%s", diff)
   548  			}
   549  		})
   550  	}
   551  }
   552  

View as plain text