blob: 2302a6c554269d93cbd010f994acada01f1190f3 [file] [edit]
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bigtable
import (
"context"
"errors"
"fmt"
"strings"
"testing"
"time"
btapb "cloud.google.com/go/bigtable/admin/apiv2/adminpb"
"cloud.google.com/go/internal/optional"
"cloud.google.com/go/internal/pretty"
"cloud.google.com/go/internal/testutil"
longrunning "cloud.google.com/go/longrunning/autogen/longrunningpb"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
type mockTableAdminClock struct {
btapb.BigtableTableAdminClient
createTableReq *btapb.CreateTableRequest
updateTableReq *btapb.UpdateTableRequest
createTableResp *btapb.Table
updateTableError error
copyBackupReq *btapb.CopyBackupRequest
copyBackupError error
modColumnReq *btapb.ModifyColumnFamiliesRequest
createAuthorizedViewReq *btapb.CreateAuthorizedViewRequest
createAuthorizedViewError error
updateAuthorizedViewReq *btapb.UpdateAuthorizedViewRequest
updateAuthorizedViewError error
getTableReq *btapb.GetTableRequest
getTableResp *btapb.Table
}
func (c *mockTableAdminClock) GetTable(
ctx context.Context, in *btapb.GetTableRequest, opts ...grpc.CallOption,
) (*btapb.Table, error) {
c.getTableReq = in
return c.getTableResp, nil
}
func (c *mockTableAdminClock) CreateTable(
ctx context.Context, in *btapb.CreateTableRequest, opts ...grpc.CallOption,
) (*btapb.Table, error) {
c.createTableReq = in
return c.createTableResp, nil
}
func (c *mockTableAdminClock) UpdateTable(
ctx context.Context, in *btapb.UpdateTableRequest, opts ...grpc.CallOption,
) (*longrunning.Operation, error) {
c.updateTableReq = in
return &longrunning.Operation{
Done: true,
Result: &longrunning.Operation_Response{
Response: &anypb.Any{TypeUrl: "google.bigtable.admin.v2.Table"},
},
}, c.updateTableError
}
func (c *mockTableAdminClock) CopyBackup(
ctx context.Context, in *btapb.CopyBackupRequest, opts ...grpc.CallOption,
) (*longrunning.Operation, error) {
c.copyBackupReq = in
c.copyBackupError = fmt.Errorf("Mock error from client API")
return nil, c.copyBackupError
}
func (c *mockTableAdminClock) ModifyColumnFamilies(
ctx context.Context, in *btapb.ModifyColumnFamiliesRequest, opts ...grpc.CallOption) (*btapb.Table, error) {
c.modColumnReq = in
return nil, nil
}
func (c *mockTableAdminClock) CreateAuthorizedView(
ctx context.Context, in *btapb.CreateAuthorizedViewRequest, opts ...grpc.CallOption,
) (*longrunning.Operation, error) {
c.createAuthorizedViewReq = in
return &longrunning.Operation{
Done: true,
Result: &longrunning.Operation_Response{
Response: &anypb.Any{TypeUrl: "google.bigtable.admin.v2.AuthorizedView"},
},
}, c.createAuthorizedViewError
}
func (c *mockTableAdminClock) UpdateAuthorizedView(
ctx context.Context, in *btapb.UpdateAuthorizedViewRequest, opts ...grpc.CallOption,
) (*longrunning.Operation, error) {
c.updateAuthorizedViewReq = in
return &longrunning.Operation{
Done: true,
Result: &longrunning.Operation_Response{
Response: &anypb.Any{TypeUrl: "google.bigtable.admin.v2.AuthorizedView"},
},
}, c.updateAuthorizedViewError
}
func setupTableClient(t *testing.T, ac btapb.BigtableTableAdminClient) *AdminClient {
ctx := context.Background()
c, err := NewAdminClient(ctx, "my-cool-project", "my-cool-instance")
if err != nil {
t.Fatalf("NewAdminClient failed: %v", err)
}
c.tClient = ac
return c
}
func TestTableAdmin_CreateTableFromConf_DeletionProtection_Protected(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
deletionProtection := Protected
err := c.CreateTableFromConf(context.Background(), &TableConf{TableID: "My-table", DeletionProtection: deletionProtection})
if err != nil {
t.Fatalf("CreateTableFromConf failed: %v", err)
}
createTableReq := mock.createTableReq
if !cmp.Equal(createTableReq.TableId, "My-table") {
t.Errorf("Unexpected table ID: %v, expected %v", createTableReq.TableId, "My-table")
}
if !cmp.Equal(createTableReq.Table.DeletionProtection, true) {
t.Errorf("Unexpected table deletion protection: %v, expected %v", createTableReq.Table.DeletionProtection, true)
}
}
func TestTableAdmin_CreateTableFromConf_DeletionProtection_Unprotected(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
deletionProtection := Unprotected
err := c.CreateTableFromConf(context.Background(), &TableConf{TableID: "My-table", DeletionProtection: deletionProtection})
if err != nil {
t.Fatalf("CreateTableFromConf failed: %v", err)
}
createTableReq := mock.createTableReq
if !cmp.Equal(createTableReq.TableId, "My-table") {
t.Errorf("Unexpected table ID: %v, expected %v", createTableReq.TableId, "My-table")
}
if !cmp.Equal(createTableReq.Table.DeletionProtection, false) {
t.Errorf("Unexpected table deletion protection: %v, expected %v", createTableReq.Table.DeletionProtection, false)
}
}
func TestTableAdmin_CreateTableFromConf_ChangeStream_Valid(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
changeStreamRetention, err := time.ParseDuration("24h")
if err != nil {
t.Fatalf("ChangeStreamRetention not valid: %v", err)
}
err = c.CreateTableFromConf(context.Background(), &TableConf{TableID: "My-table", ChangeStreamRetention: changeStreamRetention})
if err != nil {
t.Fatalf("CreateTableFromConf failed: %v", err)
}
createTableReq := mock.createTableReq
if !cmp.Equal(createTableReq.TableId, "My-table") {
t.Errorf("Unexpected table ID: %v, expected %v", createTableReq.TableId, "My-table")
}
if !cmp.Equal(createTableReq.Table.ChangeStreamConfig.RetentionPeriod.Seconds, int64(changeStreamRetention.Seconds())) {
t.Errorf("Unexpected table change stream retention: %v, expected %v", createTableReq.Table.ChangeStreamConfig.RetentionPeriod.Seconds, changeStreamRetention.Seconds())
}
}
func TestTableAdmin_CreateTableFromConf_AutomatedBackupPolicy_Valid(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
retentionPeriod, err := time.ParseDuration("72h")
if err != nil {
t.Fatalf("RetentionPeriod not valid: %v", err)
}
frequency, err := time.ParseDuration("24h")
if err != nil {
t.Fatalf("Frequency not valid: %v", err)
}
automatedBackupPolicy := TableAutomatedBackupPolicy{RetentionPeriod: retentionPeriod, Frequency: frequency}
err = c.CreateTableFromConf(context.Background(), &TableConf{TableID: "My-table", AutomatedBackupConfig: &automatedBackupPolicy})
if err != nil {
t.Fatalf("CreateTableFromConf failed: %v", err)
}
createTableReq := mock.createTableReq
if !cmp.Equal(createTableReq.TableId, "My-table") {
t.Errorf("Unexpected table ID: %v, expected %v", createTableReq.TableId, "My-table")
}
if !cmp.Equal(createTableReq.Table.GetAutomatedBackupPolicy().Frequency.Seconds, int64(automatedBackupPolicy.Frequency.(time.Duration).Seconds())) {
t.Errorf("Unexpected table automated backup policy frequency: %v, expected %v", createTableReq.Table.GetAutomatedBackupPolicy().Frequency.Seconds, automatedBackupPolicy.Frequency.(time.Duration))
}
if !cmp.Equal(createTableReq.Table.GetAutomatedBackupPolicy().RetentionPeriod.Seconds, int64(automatedBackupPolicy.RetentionPeriod.(time.Duration).Seconds())) {
t.Errorf("Unexpected table automated backup policy retention period: %v, expected %v", createTableReq.Table.GetAutomatedBackupPolicy().Frequency.Seconds, automatedBackupPolicy.Frequency.(time.Duration))
}
}
func TestTableAdmin_CreateTableFromConf_TieredStorageConfig_Valid(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
tieredStorageConfig := TieredStorageConfig{
InfrequentAccess: &TieredStorageIncludeIfOlderThan{Duration: 30 * 24 * time.Hour},
}
err := c.CreateTableFromConf(context.Background(), &TableConf{
TableID: "My-table",
TieredStorageConfig: &tieredStorageConfig,
})
if err != nil {
t.Fatalf("CreateTableFromConf failed: %v", err)
}
createTableReq := mock.createTableReq
if !cmp.Equal(createTableReq.TableId, "My-table") {
t.Errorf("Unexpected table ID: %v, expected %v", createTableReq.TableId, "My-table")
}
got := createTableReq.Table.TieredStorageConfig
if got == nil {
t.Fatal("TieredStorageConfig is nil")
}
if got.InfrequentAccess == nil {
t.Fatal("InfrequentAccess is nil")
}
if got.InfrequentAccess.GetIncludeIfOlderThan().AsDuration() != 30*24*time.Hour {
t.Errorf("Unexpected IncludeIfOlderThan: %v, expected %v", got.InfrequentAccess.GetIncludeIfOlderThan().AsDuration(), 30*24*time.Hour)
}
}
func TestTableAdmin_CreateTableFromConf_WithRowKeySchema(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
err := c.CreateTableFromConf(context.Background(), &TableConf{TableID: "my-table", RowKeySchema: &StructType{
Fields: []StructField{
{FieldName: "key1", FieldType: Int64Type{Encoding: Int64OrderedCodeBytesEncoding{}}},
{FieldName: "key2", FieldType: StringType{Encoding: StringUtf8BytesEncoding{}}},
},
Encoding: StructDelimitedBytesEncoding{Delimiter: []byte{'#', '#'}},
}})
if err != nil {
t.Fatalf("CreateTableFromConf failed: %v", err)
}
createTableReq := mock.createTableReq
if !cmp.Equal(createTableReq.TableId, "my-table") {
t.Errorf("Unexpected tableID: %v, want: %v", createTableReq.TableId, "my-table")
}
if createTableReq.Table.RowKeySchema == nil {
t.Errorf("Unexpected nil RowKeySchema in request")
}
if len(createTableReq.Table.RowKeySchema.Fields) != 2 {
t.Errorf("Unexpected field length in row key schema: %v, want: %v", createTableReq.Table.RowKeySchema, 2)
}
}
func TestTableAdmin_CreateBackupWithOptions_NoExpiryTime(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
err := c.CreateBackupWithOptions(context.Background(), "table", "cluster", "backup-01")
if err == nil || !errors.Is(err, errExpiryMissing) {
t.Errorf("CreateBackupWithOptions got: %v, want: %v error", err, errExpiryMissing)
}
}
func TestTableAdmin_CopyBackup_ErrorFromClient(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
currTime := time.Now()
err := c.CopyBackup(context.Background(), "source-cluster", "source-backup", "dest-project", "dest-instance", "dest-cluster", "dest-backup", currTime)
if err == nil {
t.Errorf("CopyBackup got: nil, want: non-nil error")
}
got := mock.copyBackupReq
want := &btapb.CopyBackupRequest{
Parent: "projects/dest-project/instances/dest-instance/clusters/dest-cluster",
BackupId: "dest-backup",
SourceBackup: "projects/my-cool-project/instances/my-cool-instance/clusters/source-cluster/backups/source-backup",
ExpireTime: timestamppb.New(currTime),
}
if diff := testutil.Diff(got, want, cmp.AllowUnexported(btapb.CopyBackupRequest{})); diff != "" {
t.Errorf("CopyBackupRequest \ngot:\n%v,\nwant:\n%v,\ndiff:\n%v", pretty.Value(got), pretty.Value(want), diff)
}
}
func TestTableAdmin_CreateTableFromConf_ChangeStream_Disable(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
changeStreamRetention := time.Duration(0)
err := c.CreateTableFromConf(context.Background(), &TableConf{TableID: "My-table", ChangeStreamRetention: changeStreamRetention})
if err != nil {
t.Fatalf("CreateTableFromConf failed: %v", err)
}
createTableReq := mock.createTableReq
if !cmp.Equal(createTableReq.TableId, "My-table") {
t.Errorf("Unexpected table ID: %v, expected %v", createTableReq.TableId, "My-table")
}
if createTableReq.Table.ChangeStreamConfig != nil {
t.Errorf("Unexpected table change stream retention: %v should be empty", createTableReq.Table.ChangeStreamConfig)
}
}
func TestTableAdmin_CreateTableFromConf_AutomatedBackupPolicy_Disable(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
err := c.CreateTableFromConf(context.Background(), &TableConf{TableID: "My-table", AutomatedBackupConfig: nil})
if err != nil {
t.Fatalf("CreateTableFromConf failed: %v", err)
}
createTableReq := mock.createTableReq
if !cmp.Equal(createTableReq.TableId, "My-table") {
t.Errorf("Unexpected table ID: %v, expected %v", createTableReq.TableId, "My-table")
}
if createTableReq.Table.AutomatedBackupConfig != nil {
t.Errorf("Unexpected table automated backup policy %v should be empty", createTableReq.Table.AutomatedBackupConfig)
}
if createTableReq.Table.GetAutomatedBackupPolicy() != nil {
t.Errorf("Unexpected table automated backup policy %v should be empty", createTableReq.Table.GetAutomatedBackupPolicy())
}
}
func TestTableAdmin_UpdateTableWithDeletionProtection(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
deletionProtection := Protected
// Check if the deletion protection updates correctly
err := c.UpdateTableWithDeletionProtection(context.Background(), "My-table", deletionProtection)
if err != nil {
t.Fatalf("UpdateTableWithDeletionProtection failed: %v", err)
}
updateTableReq := mock.updateTableReq
if !cmp.Equal(updateTableReq.Table.Name, "projects/my-cool-project/instances/my-cool-instance/tables/My-table") {
t.Errorf("UpdateTableRequest does not match, TableID: %v", updateTableReq.Table.Name)
}
if !cmp.Equal(updateTableReq.Table.DeletionProtection, true) {
t.Errorf("UpdateTableRequest does not match, TableID: %v", updateTableReq.Table.Name)
}
if !cmp.Equal(len(updateTableReq.UpdateMask.Paths), 1) {
t.Errorf("UpdateTableRequest does not match, UpdateMask has length of %d, expected 1", len(updateTableReq.UpdateMask.Paths))
}
if !cmp.Equal(updateTableReq.UpdateMask.Paths[0], "deletion_protection") {
t.Errorf("UpdateTableRequest does not match, TableID: %v", updateTableReq.Table.Name)
}
}
func TestTableAdmin_UpdateTable_WithError(t *testing.T) {
mock := &mockTableAdminClock{updateTableError: errors.New("update table failure error")}
c := setupTableClient(t, mock)
deletionProtection := Protected
// Check if the update fails when update table returns an error
err := c.UpdateTableWithDeletionProtection(context.Background(), "My-table", deletionProtection)
if fmt.Sprint(err) != "error from update: update table failure error" {
t.Fatalf("UpdateTable updated by mistake: %v", err)
}
}
func TestTableAdmin_UpdateTable_TableID_NotProvided(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
deletionProtection := Protected
// Check if the update fails when TableID is not provided
err := c.UpdateTableWithDeletionProtection(context.Background(), "", deletionProtection)
if !strings.Contains(fmt.Sprint(err), "TableID is required") {
t.Fatalf("UpdateTable failed: %v", err)
}
}
func TestTableAdmin_UpdateTableWithChangeStreamRetention(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
changeStreamRetention, err := time.ParseDuration("24h")
if err != nil {
t.Fatalf("ChangeStreamRetention not valid: %v", err)
}
err = c.UpdateTableWithChangeStream(context.Background(), "My-table", changeStreamRetention)
if err != nil {
t.Fatalf("UpdateTableWithChangeStream failed: %v", err)
}
updateTableReq := mock.updateTableReq
if !cmp.Equal(updateTableReq.Table.Name, "projects/my-cool-project/instances/my-cool-instance/tables/My-table") {
t.Errorf("UpdateTableRequest does not match, TableID: %v", updateTableReq.Table.Name)
}
if !cmp.Equal(updateTableReq.Table.ChangeStreamConfig.RetentionPeriod.Seconds, int64(changeStreamRetention.Seconds())) {
t.Errorf("UpdateTableRequest does not match, ChangeStreamConfig: %v", updateTableReq.Table.ChangeStreamConfig)
}
if !cmp.Equal(len(updateTableReq.UpdateMask.Paths), 1) {
t.Errorf("UpdateTableRequest does not match, UpdateMask has length of %d, expected 1", len(updateTableReq.UpdateMask.Paths))
}
if !cmp.Equal(updateTableReq.UpdateMask.Paths[0], "change_stream_config.retention_period") {
t.Errorf("UpdateTableRequest does not match, UpdateMask: %v", updateTableReq.UpdateMask.Paths[0])
}
}
func TestTableAdmin_UpdateTableDisableChangeStream(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
err := c.UpdateTableDisableChangeStream(context.Background(), "My-table")
if err != nil {
t.Fatalf("UpdateTableDisableChangeStream failed: %v", err)
}
updateTableReq := mock.updateTableReq
if !cmp.Equal(updateTableReq.Table.Name, "projects/my-cool-project/instances/my-cool-instance/tables/My-table") {
t.Errorf("UpdateTableRequest does not match, TableID: %v", updateTableReq.Table.Name)
}
if updateTableReq.Table.ChangeStreamConfig != nil {
t.Errorf("UpdateTableRequest does not match, ChangeStreamConfig: %v should be empty", updateTableReq.Table.ChangeStreamConfig)
}
if !cmp.Equal(len(updateTableReq.UpdateMask.Paths), 1) {
t.Errorf("UpdateTableRequest does not match, UpdateMask has length of %d, expected 1", len(updateTableReq.UpdateMask.Paths))
}
if !cmp.Equal(updateTableReq.UpdateMask.Paths[0], "change_stream_config") {
t.Errorf("UpdateTableRequest does not match, UpdateMask: %v", updateTableReq.UpdateMask.Paths[0])
}
}
func TestTableAdmin_UpdateTableWithRowKeySchema(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
rks := StructType{
Fields: []StructField{
{FieldName: "key1", FieldType: Int64Type{Encoding: Int64OrderedCodeBytesEncoding{}}},
{FieldName: "key2", FieldType: StringType{Encoding: StringUtf8BytesEncoding{}}},
},
Encoding: StructDelimitedBytesEncoding{Delimiter: []byte{'#', '#'}}}
err := c.UpdateTableWithRowKeySchema(context.Background(), "my-table", rks)
if err != nil {
t.Fatalf("UpdateTableWithRowKeySchema error: %v", err)
}
req := mock.updateTableReq
expectedTableName := "projects/my-cool-project/instances/my-cool-instance/tables/my-table"
if !cmp.Equal(req.Table.Name, expectedTableName) {
t.Errorf("Unexpected table name: %v, want: %v", req.Table.Name, expectedTableName)
}
if !cmp.Equal(len(req.UpdateMask.Paths), 1) {
t.Errorf("Unexpected mask size: %v, want: %v", len(req.UpdateMask.Paths), 1)
}
if !cmp.Equal(req.UpdateMask.Paths[0], "row_key_schema") {
t.Errorf("Unexpected mask path: %v, want: %v", req.UpdateMask.Paths[0], "row_key_schema")
}
}
func TestTableAdmin_UpdateTableWithClearRowKeySchema(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
err := c.UpdateTableRemoveRowKeySchema(context.Background(), "my-table")
if err != nil {
t.Fatalf("UpdateTableWithRowKeySchema error: %v", err)
}
req := mock.updateTableReq
expectedTableName := "projects/my-cool-project/instances/my-cool-instance/tables/my-table"
if !cmp.Equal(req.Table.Name, expectedTableName) {
t.Errorf("Unexpected table name: %v, want: %v", req.Table.Name, expectedTableName)
}
if !cmp.Equal(len(req.UpdateMask.Paths), 1) {
t.Errorf("Unexpected mask size: %v, want: %v", len(req.UpdateMask.Paths), 1)
}
if !cmp.Equal(req.UpdateMask.Paths[0], "row_key_schema") {
t.Errorf("Unexpected mask path: %v, want: %v", req.UpdateMask.Paths[0], "row_key_schema")
}
if req.Table.RowKeySchema != nil {
t.Errorf("Unexpected row key schema in table during clear schema request: %v", req.Table.RowKeySchema)
}
if !req.IgnoreWarnings {
t.Errorf("Expect IgnoreWarnings set to true when clearing row key schema")
}
}
func TestTableAdmin_SetGcPolicy(t *testing.T) {
for _, test := range []struct {
desc string
opts UpdateFamilyOption
want bool
}{
{
desc: "IgnoreWarnings: false",
want: false,
},
{
desc: "IgnoreWarnings: true",
opts: IgnoreWarnings(),
want: true,
},
} {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
err := c.SetGCPolicyWithOptions(context.Background(), "My-table", "cf1", NoGcPolicy(), test.opts)
if err != nil {
t.Fatalf("%v: Failed to set GC Policy: %v", test.desc, err)
}
modColumnReq := mock.modColumnReq
if modColumnReq.IgnoreWarnings != test.want {
t.Errorf("%v: IgnoreWarnings got: %v, want: %v", test.desc, modColumnReq.IgnoreWarnings, test.want)
}
}
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
err := c.SetGCPolicy(context.Background(), "My-table", "cf1", NoGcPolicy())
if err != nil {
t.Fatalf("SetGCPolicy: Failed to set GC Policy: %v", err)
}
modColumnReq := mock.modColumnReq
if modColumnReq.IgnoreWarnings {
t.Errorf("SetGCPolicy: IgnoreWarnings should be set to false")
}
}
func TestTableAdmin_CreateAuthorizedView_DeletionProtection_Protected(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
err := c.CreateTableFromConf(context.Background(), &TableConf{TableID: "my-cool-table"})
if err != nil {
t.Fatalf("CreateTableFromConf failed: %v", err)
}
deletionProtection := Protected
err = c.CreateAuthorizedView(context.Background(), &AuthorizedViewConf{
TableID: "my-cool-table",
AuthorizedViewID: "my-cool-authorized-view",
AuthorizedView: &SubsetViewConf{},
DeletionProtection: deletionProtection,
})
if err != nil {
t.Fatalf("CreateAuthorizedView failed: %v", err)
}
createAuthorizedViewReq := mock.createAuthorizedViewReq
if !cmp.Equal(createAuthorizedViewReq.Parent, "projects/my-cool-project/instances/my-cool-instance/tables/my-cool-table") {
t.Errorf("Unexpected parent: %v, expected %v", createAuthorizedViewReq.Parent, "projects/my-cool-project/instances/my-cool-instance/tables/my-cool-table")
}
if !cmp.Equal(createAuthorizedViewReq.AuthorizedViewId, "my-cool-authorized-view") {
t.Errorf("Unexpected authorized view ID: %v, expected %v", createAuthorizedViewReq.Parent, "my-cool-authorized-view")
}
if !cmp.Equal(createAuthorizedViewReq.AuthorizedView.DeletionProtection, true) {
t.Errorf("Unexpected authorized view deletion protection: %v, expected %v", createAuthorizedViewReq.AuthorizedView.DeletionProtection, true)
}
}
func TestTableAdmin_CreateAuthorizedView_DeletionProtection_Unprotected(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
deletionProtection := Unprotected
err := c.CreateAuthorizedView(context.Background(), &AuthorizedViewConf{
TableID: "my-cool-table",
AuthorizedViewID: "my-cool-authorized-view",
AuthorizedView: &SubsetViewConf{},
DeletionProtection: deletionProtection,
})
if err != nil {
t.Fatalf("CreateAuthorizedView failed: %v", err)
}
createAuthorizedViewReq := mock.createAuthorizedViewReq
if !cmp.Equal(createAuthorizedViewReq.Parent, "projects/my-cool-project/instances/my-cool-instance/tables/my-cool-table") {
t.Errorf("Unexpected parent: %v, expected %v", createAuthorizedViewReq.Parent, "projects/my-cool-project/instances/my-cool-instance/tables/my-cool-table")
}
if !cmp.Equal(createAuthorizedViewReq.AuthorizedViewId, "my-cool-authorized-view") {
t.Errorf("Unexpected authorized view ID: %v, expected %v", createAuthorizedViewReq.Parent, "my-cool-authorized-view")
}
if !cmp.Equal(createAuthorizedViewReq.AuthorizedView.DeletionProtection, false) {
t.Errorf("Unexpected authorized view deletion protection: %v, expected %v", createAuthorizedViewReq.AuthorizedView.DeletionProtection, false)
}
}
func TestTableAdmin_UpdateAuthorizedViewWithDeletionProtection(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
deletionProtection := Protected
// Check if the deletion protection updates correctly
err := c.UpdateAuthorizedView(context.Background(), UpdateAuthorizedViewConf{
AuthorizedViewConf: AuthorizedViewConf{
TableID: "my-cool-table",
AuthorizedViewID: "my-cool-authorized-view",
DeletionProtection: deletionProtection,
},
})
if err != nil {
t.Fatalf("UpdateAuthorizedView failed: %v", err)
}
updateAuthorizedViewReq := mock.updateAuthorizedViewReq
if !cmp.Equal(updateAuthorizedViewReq.AuthorizedView.Name, "projects/my-cool-project/instances/my-cool-instance/tables/my-cool-table/authorizedViews/my-cool-authorized-view") {
t.Errorf("UpdateAuthorizedViewRequest does not match: AuthorizedViewName: %v, expected %v", updateAuthorizedViewReq.AuthorizedView.Name, "projects/my-cool-project/instances/my-cool-instance/tables/my-cool-table/authorizedViews/my-cool-authorized-view")
}
if !cmp.Equal(updateAuthorizedViewReq.AuthorizedView.DeletionProtection, true) {
t.Errorf("UpdateAuthorizedViewRequest does not match: DeletionProtection: %v, expected %v", updateAuthorizedViewReq.AuthorizedView.DeletionProtection, true)
}
if !cmp.Equal(len(updateAuthorizedViewReq.UpdateMask.Paths), 1) {
t.Errorf("UpdateAuthorizedViewRequest does not match: UpdateMask has length of %d, expected %v", len(updateAuthorizedViewReq.UpdateMask.Paths), 1)
}
if !cmp.Equal(updateAuthorizedViewReq.UpdateMask.Paths[0], "deletion_protection") {
t.Errorf("UpdateAuthorizedViewRequest does not match: updateAuthorizedViewReq.UpdateMask.Paths[0]: %v, expected: %v", updateAuthorizedViewReq.UpdateMask.Paths[0], "deletion_protection")
}
}
func TestTableAdmin_UpdateAuthorizedViewWithSubsetView(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
err := c.UpdateAuthorizedView(context.Background(), UpdateAuthorizedViewConf{
AuthorizedViewConf: AuthorizedViewConf{
TableID: "my-cool-table",
AuthorizedViewID: "my-cool-authorized-view",
AuthorizedView: &SubsetViewConf{},
},
})
if err != nil {
t.Fatalf("UpdateAuthorizedView failed: %v", err)
}
updateAuthorizedViewReq := mock.updateAuthorizedViewReq
if !cmp.Equal(updateAuthorizedViewReq.AuthorizedView.Name, "projects/my-cool-project/instances/my-cool-instance/tables/my-cool-table/authorizedViews/my-cool-authorized-view") {
t.Errorf("UpdateAuthorizedViewRequest does not match: AuthorizedViewName: %v, expected %v", updateAuthorizedViewReq.AuthorizedView.Name, "projects/my-cool-project/instances/my-cool-instance/tables/my-cool-table/authorizedViews/my-cool-authorized-view")
}
if !cmp.Equal(len(updateAuthorizedViewReq.UpdateMask.Paths), 1) {
t.Errorf("UpdateAuthorizedViewRequest does not match: UpdateMask has length of %d, expected %v", len(updateAuthorizedViewReq.UpdateMask.Paths), 1)
}
if !cmp.Equal(updateAuthorizedViewReq.UpdateMask.Paths[0], "subset_view") {
t.Errorf("UpdateAuthorizedViewRequest does not match: updateAuthorizedViewReq.UpdateMask.Paths[0]: %v, expected: %v", updateAuthorizedViewReq.UpdateMask.Paths[0], "subset_view")
}
}
func TestTableAdmin_UpdateTableWithAutomatedBackupPolicy_Valid(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
retentionPeriod, err := time.ParseDuration("72h")
if err != nil {
t.Fatalf("RetentionPeriod not valid: %v", err)
}
frequency, err := time.ParseDuration("24h")
if err != nil {
t.Fatalf("Frequency not valid: %v", err)
}
automatedBackupPolicy := TableAutomatedBackupPolicy{RetentionPeriod: retentionPeriod, Frequency: frequency}
err = c.UpdateTableWithAutomatedBackupPolicy(context.Background(), "My-table", automatedBackupPolicy)
if err != nil {
t.Fatalf("UpdateTableWithChangeStream failed: %v", err)
}
updateTableReq := mock.updateTableReq
if !cmp.Equal(updateTableReq.Table.Name, "projects/my-cool-project/instances/my-cool-instance/tables/My-table") {
t.Errorf("UpdateTableRequest does not match, TableID: %v", updateTableReq.Table.Name)
}
if !cmp.Equal(updateTableReq.Table.GetAutomatedBackupPolicy().RetentionPeriod.Seconds, int64(automatedBackupPolicy.RetentionPeriod.(time.Duration).Seconds())) {
t.Errorf("UpdateTableRequest does not match, AutomatedBackupPolicy.RetentionPeriod: %v", updateTableReq.Table.GetAutomatedBackupPolicy().RetentionPeriod)
}
if !cmp.Equal(updateTableReq.Table.GetAutomatedBackupPolicy().Frequency.Seconds, int64(automatedBackupPolicy.Frequency.(time.Duration).Seconds())) {
t.Errorf("UpdateTableRequest does not match, AutomatedBackupPolicy.Frequency: %v", updateTableReq.Table.GetAutomatedBackupPolicy().Frequency)
}
if !cmp.Equal(len(updateTableReq.UpdateMask.Paths), 2) {
t.Errorf("UpdateTableRequest does not match, UpdateMask has length of %d, expected 1", len(updateTableReq.UpdateMask.Paths))
}
if !cmp.Equal(updateTableReq.UpdateMask.Paths[0], "automated_backup_policy.retention_period") {
t.Errorf("UpdateTableRequest does not match, UpdateMask: %v", updateTableReq.UpdateMask.Paths[0])
}
if !cmp.Equal(updateTableReq.UpdateMask.Paths[1], "automated_backup_policy.frequency") {
t.Errorf("UpdateTableRequest does not match, UpdateMask: %v", updateTableReq.UpdateMask.Paths[1])
}
}
func TestTableAdmin_UpdateTableWithAutomatedBackupPolicy_JustFrequency_Valid(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
frequency, err := time.ParseDuration("24h")
if err != nil {
t.Fatalf("Frequency not valid: %v", err)
}
automatedBackupPolicy := TableAutomatedBackupPolicy{Frequency: frequency}
err = c.UpdateTableWithAutomatedBackupPolicy(context.Background(), "My-table", automatedBackupPolicy)
if err != nil {
t.Fatalf("UpdateTableWithChangeStream failed: %v", err)
}
updateTableReq := mock.updateTableReq
if !cmp.Equal(updateTableReq.Table.Name, "projects/my-cool-project/instances/my-cool-instance/tables/My-table") {
t.Errorf("UpdateTableRequest does not match, TableID: %v", updateTableReq.Table.Name)
}
if !cmp.Equal(updateTableReq.Table.GetAutomatedBackupPolicy().Frequency.Seconds, int64(automatedBackupPolicy.Frequency.(time.Duration).Seconds())) {
t.Errorf("UpdateTableRequest does not match, AutomatedBackupPolicy.Frequency: %v", updateTableReq.Table.GetAutomatedBackupPolicy().Frequency)
}
if !cmp.Equal(len(updateTableReq.UpdateMask.Paths), 1) {
t.Errorf("UpdateTableRequest does not match, UpdateMask has length of %d, expected 1", len(updateTableReq.UpdateMask.Paths))
}
if !cmp.Equal(updateTableReq.UpdateMask.Paths[0], "automated_backup_policy.frequency") {
t.Errorf("UpdateTableRequest does not match, UpdateMask: %v", updateTableReq.UpdateMask.Paths[0])
}
}
func TestTableAdmin_UpdateTableWithAutomatedBackupPolicy_JustRetentionPeriod_Valid(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
retentionPeriod, err := time.ParseDuration("72h")
if err != nil {
t.Fatalf("RetentionPeriod not valid: %v", err)
}
automatedBackupPolicy := TableAutomatedBackupPolicy{RetentionPeriod: retentionPeriod}
err = c.UpdateTableWithAutomatedBackupPolicy(context.Background(), "My-table", automatedBackupPolicy)
if err != nil {
t.Fatalf("UpdateTableWithChangeStream failed: %v", err)
}
updateTableReq := mock.updateTableReq
if !cmp.Equal(updateTableReq.Table.Name, "projects/my-cool-project/instances/my-cool-instance/tables/My-table") {
t.Errorf("UpdateTableRequest does not match, TableID: %v", updateTableReq.Table.Name)
}
if !cmp.Equal(updateTableReq.Table.GetAutomatedBackupPolicy().RetentionPeriod.Seconds, int64(automatedBackupPolicy.RetentionPeriod.(time.Duration).Seconds())) {
t.Errorf("UpdateTableRequest does not match, AutomatedBackupPolicy.RetentionPeriod: %v", updateTableReq.Table.GetAutomatedBackupPolicy().RetentionPeriod)
}
if !cmp.Equal(len(updateTableReq.UpdateMask.Paths), 1) {
t.Errorf("UpdateTableRequest does not match, UpdateMask has length of %d, expected 1", len(updateTableReq.UpdateMask.Paths))
}
if !cmp.Equal(updateTableReq.UpdateMask.Paths[0], "automated_backup_policy.retention_period") {
t.Errorf("UpdateTableRequest does not match, UpdateMask: %v", updateTableReq.UpdateMask.Paths[0])
}
}
func TestTableAdmin_UpdateTableDisableAutomatedBackupPolicy(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
err := c.UpdateTableDisableAutomatedBackupPolicy(context.Background(), "My-table")
if err != nil {
t.Fatalf("UpdateTableDisableAutomatedBackupPolicy failed: %v", err)
}
updateTableReq := mock.updateTableReq
if !cmp.Equal(updateTableReq.Table.Name, "projects/my-cool-project/instances/my-cool-instance/tables/My-table") {
t.Errorf("UpdateTableRequest does not match, TableID: %v", updateTableReq.Table.Name)
}
if updateTableReq.Table.AutomatedBackupConfig != nil {
t.Errorf("UpdateTableRequest does not match, AutomatedBackupConfig: %v should be empty", updateTableReq.Table.AutomatedBackupConfig)
}
if updateTableReq.Table.GetAutomatedBackupPolicy() != nil {
t.Errorf("UpdateTableRequest does not match, GetAutomatedBackupPolicy: %v should be empty", updateTableReq.Table.GetAutomatedBackupPolicy())
}
if !cmp.Equal(len(updateTableReq.UpdateMask.Paths), 1) {
t.Errorf("UpdateTableRequest does not match, UpdateMask has length of %d, expected 1", len(updateTableReq.UpdateMask.Paths))
}
if !cmp.Equal(updateTableReq.UpdateMask.Paths[0], "automated_backup_policy") {
t.Errorf("UpdateTableRequest does not match, UpdateMask: %v", updateTableReq.UpdateMask.Paths[0])
}
}
func TestTableAdmin_UpdateTableWithAutomatedBackupPolicy_NilFields_Invalid(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
err := c.UpdateTableWithAutomatedBackupPolicy(context.Background(), "My-table", TableAutomatedBackupPolicy{nil, nil})
if err == nil {
t.Fatalf("Expected UpdateTableDisableAutomatedBackupPolicy to fail due to misspecified AutomatedBackupPolicy")
}
}
func TestTableAdmin_UpdateTableWithAutomatedBackupPolicy_ZeroFields_Invalid(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
err := c.UpdateTableWithAutomatedBackupPolicy(context.Background(), "My-table", TableAutomatedBackupPolicy{time.Duration(0), time.Duration(0)})
if err == nil {
t.Fatalf("Expected UpdateTableDisableAutomatedBackupPolicy to fail due to misspecified AutomatedBackupPolicy")
}
}
func TestTableAdmin_UpdateTableWithTieredStorageConfig(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
tieredStorageConfig := TieredStorageConfig{
InfrequentAccess: &TieredStorageIncludeIfOlderThan{Duration: 30 * 24 * time.Hour},
}
err := c.UpdateTableWithTieredStorageConfig(context.Background(), "My-table", &tieredStorageConfig)
if err != nil {
t.Fatalf("UpdateTableWithTieredStorageConfig failed: %v", err)
}
updateTableReq := mock.updateTableReq
if updateTableReq.UpdateMask.Paths[0] != tieredStorageConfigFieldMask {
t.Errorf("Unexpected update mask: %v, expected %v", updateTableReq.UpdateMask.Paths[0], tieredStorageConfigFieldMask)
}
got := updateTableReq.Table.TieredStorageConfig
if got == nil {
t.Fatal("TieredStorageConfig is nil")
}
if got.InfrequentAccess == nil {
t.Fatal("InfrequentAccess is nil")
}
if got.InfrequentAccess.GetIncludeIfOlderThan().AsDuration() != 30*24*time.Hour {
t.Errorf("Unexpected IncludeIfOlderThan: %v, expected %v", got.InfrequentAccess.GetIncludeIfOlderThan().AsDuration(), 30*24*time.Hour)
}
}
func TestTableAdmin_TableInfo_TieredStorageConfig(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
mock.getTableResp = &btapb.Table{
Name: "projects/my-cool-project/instances/my-cool-instance/tables/My-table",
TieredStorageConfig: &btapb.TieredStorageConfig{
InfrequentAccess: &btapb.TieredStorageRule{
Rule: &btapb.TieredStorageRule_IncludeIfOlderThan{
IncludeIfOlderThan: durationpb.New(30 * 24 * time.Hour),
},
},
},
}
ti, err := c.TableInfo(context.Background(), "My-table")
if err != nil {
t.Fatalf("TableInfo failed: %v", err)
}
if ti.TieredStorageConfig == nil {
t.Fatal("TieredStorageConfig is nil")
}
if ti.TieredStorageConfig.InfrequentAccess == nil {
t.Fatal("InfrequentAccess is nil")
}
rule, ok := ti.TieredStorageConfig.InfrequentAccess.(*TieredStorageIncludeIfOlderThan)
if !ok {
t.Fatalf("Unexpected rule type: %T", ti.TieredStorageConfig.InfrequentAccess)
}
if optional.ToDuration(rule.Duration) != 30*24*time.Hour {
t.Errorf("Unexpected IncludeIfOlderThan: %v, expected %v", optional.ToDuration(rule.Duration), 30*24*time.Hour)
}
}
func TestTableAdmin_UpdateTableRemoveTieredStorageConfig(t *testing.T) {
mock := &mockTableAdminClock{}
c := setupTableClient(t, mock)
err := c.UpdateTableRemoveTieredStorageConfig(context.Background(), "My-table")
if err != nil {
t.Fatalf("UpdateTableRemoveTieredStorageConfig failed: %v", err)
}
updateTableReq := mock.updateTableReq
if updateTableReq.UpdateMask.Paths[0] != tieredStorageConfigFieldMask {
t.Errorf("Unexpected update mask: %v, expected %v", updateTableReq.UpdateMask.Paths[0], tieredStorageConfigFieldMask)
}
if updateTableReq.Table.TieredStorageConfig != nil {
t.Errorf("TieredStorageConfig should be nil")
}
}
type mockAdminClock struct {
btapb.BigtableInstanceAdminClient
createInstanceReq *btapb.CreateInstanceRequest
createClusterReq *btapb.CreateClusterRequest
partialUpdateClusterReq *btapb.PartialUpdateClusterRequest
getClusterResp *btapb.Cluster
createAppProfileReq *btapb.CreateAppProfileRequest
updateAppProfileReq *btapb.UpdateAppProfileRequest
}
func (c *mockAdminClock) PartialUpdateCluster(
ctx context.Context, in *btapb.PartialUpdateClusterRequest, opts ...grpc.CallOption,
) (*longrunning.Operation, error) {
c.partialUpdateClusterReq = in
return &longrunning.Operation{
Done: true,
Result: &longrunning.Operation_Response{},
}, nil
}
func (c *mockAdminClock) CreateInstance(
ctx context.Context, in *btapb.CreateInstanceRequest, opts ...grpc.CallOption,
) (*longrunning.Operation, error) {
c.createInstanceReq = in
return &longrunning.Operation{
Done: true,
Result: &longrunning.Operation_Response{
Response: &anypb.Any{TypeUrl: "google.bigtable.admin.v2.Instance"},
},
}, nil
}
func (c *mockAdminClock) CreateCluster(
ctx context.Context, in *btapb.CreateClusterRequest, opts ...grpc.CallOption,
) (*longrunning.Operation, error) {
c.createClusterReq = in
return &longrunning.Operation{
Done: true,
Result: &longrunning.Operation_Response{
Response: &anypb.Any{TypeUrl: "google.bigtable.admin.v2.Cluster"},
},
}, nil
}
func (c *mockAdminClock) PartialUpdateInstance(
ctx context.Context, in *btapb.PartialUpdateInstanceRequest, opts ...grpc.CallOption,
) (*longrunning.Operation, error) {
return &longrunning.Operation{
Done: true,
Result: &longrunning.Operation_Response{},
}, nil
}
func (c *mockAdminClock) GetCluster(
ctx context.Context, in *btapb.GetClusterRequest, opts ...grpc.CallOption,
) (*btapb.Cluster, error) {
return c.getClusterResp, nil
}
func (c *mockAdminClock) ListClusters(
ctx context.Context, in *btapb.ListClustersRequest, opts ...grpc.CallOption,
) (*btapb.ListClustersResponse, error) {
return &btapb.ListClustersResponse{Clusters: []*btapb.Cluster{c.getClusterResp}}, nil
}
func (c *mockAdminClock) CreateAppProfile(
ctx context.Context, in *btapb.CreateAppProfileRequest, opts ...grpc.CallOption,
) (*btapb.AppProfile, error) {
c.createAppProfileReq = in
return in.AppProfile, nil
}
func (c *mockAdminClock) UpdateAppProfile(
ctx context.Context, in *btapb.UpdateAppProfileRequest, opts ...grpc.CallOption,
) (*longrunning.Operation, error) {
c.updateAppProfileReq = in
return &longrunning.Operation{
Done: true,
Result: &longrunning.Operation_Response{},
}, nil
}
func setupClient(t *testing.T, ac btapb.BigtableInstanceAdminClient) *InstanceAdminClient {
ctx := context.Background()
c, err := NewInstanceAdminClient(ctx, "my-cool-project")
if err != nil {
t.Fatalf("NewInstanceAdminClient failed: %v", err)
}
c.iClient = ac
return c
}
func TestInstanceAdmin_GetCluster(t *testing.T) {
tcs := []struct {
cluster *btapb.Cluster
wantConfig *AutoscalingConfig
desc string
}{
{
desc: "when autoscaling is not enabled",
cluster: &btapb.Cluster{
Name: ".../mycluster",
Location: ".../us-central1-a",
State: btapb.Cluster_READY,
DefaultStorageType: btapb.StorageType_SSD,
NodeScalingFactor: btapb.Cluster_NODE_SCALING_FACTOR_1X,
},
wantConfig: nil,
},
{
desc: "when autoscaling is enabled",
cluster: &btapb.Cluster{
Name: ".../mycluster",
Location: ".../us-central1-a",
State: btapb.Cluster_READY,
DefaultStorageType: btapb.StorageType_SSD,
NodeScalingFactor: btapb.Cluster_NODE_SCALING_FACTOR_1X,
Config: &btapb.Cluster_ClusterConfig_{
ClusterConfig: &btapb.Cluster_ClusterConfig{
ClusterAutoscalingConfig: &btapb.Cluster_ClusterAutoscalingConfig{
AutoscalingLimits: &btapb.AutoscalingLimits{
MinServeNodes: 1,
MaxServeNodes: 2,
},
AutoscalingTargets: &btapb.AutoscalingTargets{
CpuUtilizationPercent: 10,
StorageUtilizationGibPerNode: 3000,
},
},
},
},
},
wantConfig: &AutoscalingConfig{MinNodes: 1, MaxNodes: 2, CPUTargetPercent: 10, StorageUtilizationPerNode: 3000},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
c := setupClient(t, &mockAdminClock{getClusterResp: tc.cluster})
info, err := c.GetCluster(context.Background(), "myinst", "mycluster")
if err != nil {
t.Fatalf("GetCluster failed: %v", err)
}
if gotConfig := info.AutoscalingConfig; !cmp.Equal(gotConfig, tc.wantConfig) {
t.Fatalf("want autoscaling config = %v, got = %v", tc.wantConfig, gotConfig)
}
})
}
}
func TestInstanceAdmin_Clusters(t *testing.T) {
tcs := []struct {
cluster *btapb.Cluster
wantConfig *AutoscalingConfig
desc string
}{
{
desc: "when autoscaling is not enabled",
cluster: &btapb.Cluster{
Name: ".../mycluster",
Location: ".../us-central1-a",
State: btapb.Cluster_READY,
DefaultStorageType: btapb.StorageType_SSD,
NodeScalingFactor: btapb.Cluster_NODE_SCALING_FACTOR_1X,
},
wantConfig: nil,
},
{
desc: "when autoscaling is enabled",
cluster: &btapb.Cluster{
Name: ".../mycluster",
Location: ".../us-central1-a",
State: btapb.Cluster_READY,
DefaultStorageType: btapb.StorageType_SSD,
NodeScalingFactor: btapb.Cluster_NODE_SCALING_FACTOR_1X,
Config: &btapb.Cluster_ClusterConfig_{
ClusterConfig: &btapb.Cluster_ClusterConfig{
ClusterAutoscalingConfig: &btapb.Cluster_ClusterAutoscalingConfig{
AutoscalingLimits: &btapb.AutoscalingLimits{
MinServeNodes: 1,
MaxServeNodes: 2,
},
AutoscalingTargets: &btapb.AutoscalingTargets{
CpuUtilizationPercent: 10,
StorageUtilizationGibPerNode: 3000,
},
},
},
},
},
wantConfig: &AutoscalingConfig{MinNodes: 1, MaxNodes: 2, CPUTargetPercent: 10, StorageUtilizationPerNode: 3000},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
c := setupClient(t, &mockAdminClock{getClusterResp: tc.cluster})
infos, err := c.Clusters(context.Background(), "myinst")
if err != nil {
t.Fatalf("Clusters failed: %v", err)
}
if len(infos) != 1 {
t.Fatalf("Clusters len: want = 1, got = %v", len(infos))
}
info := infos[0]
if gotConfig := info.AutoscalingConfig; !cmp.Equal(gotConfig, tc.wantConfig) {
t.Fatalf("want autoscaling config = %v, got = %v", tc.wantConfig, gotConfig)
}
})
}
}
func TestInstanceAdmin_SetAutoscaling(t *testing.T) {
mock := &mockAdminClock{}
c := setupClient(t, mock)
err := c.SetAutoscaling(context.Background(), "myinst", "mycluster", AutoscalingConfig{
MinNodes: 1,
MaxNodes: 2,
CPUTargetPercent: 10,
StorageUtilizationPerNode: 3000,
})
if err != nil {
t.Fatalf("SetAutoscaling failed: %v", err)
}
wantMask := []string{"cluster_config.cluster_autoscaling_config"}
if gotMask := mock.partialUpdateClusterReq.UpdateMask.Paths; !cmp.Equal(wantMask, gotMask) {
t.Fatalf("want update mask = %v, got = %v", wantMask, gotMask)
}
wantName := "projects/my-cool-project/instances/myinst/clusters/mycluster"
if gotName := mock.partialUpdateClusterReq.Cluster.Name; gotName != wantName {
t.Fatalf("want name = %v, got = %v", wantName, gotName)
}
cc := mock.partialUpdateClusterReq.Cluster.Config.(*btapb.Cluster_ClusterConfig_)
gotConfig := cc.ClusterConfig.ClusterAutoscalingConfig
wantMin := int32(1)
if gotMin := gotConfig.AutoscalingLimits.MinServeNodes; wantMin != gotMin {
t.Fatalf("want autoscaling min nodes = %v, got = %v", wantMin, gotMin)
}
wantMax := int32(2)
if gotMax := gotConfig.AutoscalingLimits.MaxServeNodes; wantMax != gotMax {
t.Fatalf("want autoscaling max nodes = %v, got = %v", wantMax, gotMax)
}
wantCPU := int32(10)
if gotCPU := gotConfig.AutoscalingTargets.CpuUtilizationPercent; wantCPU != gotCPU {
t.Fatalf("want autoscaling cpu = %v, got = %v", wantCPU, gotCPU)
}
wantStorage := int32(3000)
if gotStorage := gotConfig.AutoscalingTargets.StorageUtilizationGibPerNode; wantStorage != gotStorage {
t.Fatalf("want autoscaling storage = %v, got = %v", wantStorage, gotStorage)
}
}
func TestInstanceAdmin_UpdateCluster_RemovingAutoscaling(t *testing.T) {
mock := &mockAdminClock{}
c := setupClient(t, mock)
err := c.UpdateCluster(context.Background(), "myinst", "mycluster", 1)
if err != nil {
t.Fatalf("UpdateCluster failed: %v", err)
}
wantMask := []string{"serve_nodes", "cluster_config.cluster_autoscaling_config"}
if gotMask := mock.partialUpdateClusterReq.UpdateMask.Paths; !cmp.Equal(wantMask, gotMask) {
t.Fatalf("want update mask = %v, got = %v", wantMask, gotMask)
}
if gotConfig := mock.partialUpdateClusterReq.Cluster.Config; gotConfig != nil {
t.Fatalf("want config = nil, got = %v", gotConfig)
}
}
func TestInstanceAdmin_CreateInstance_WithAutoscaling(t *testing.T) {
mock := &mockAdminClock{}
c := setupClient(t, mock)
err := c.CreateInstance(context.Background(), &InstanceConf{
InstanceId: "myinst",
DisplayName: "myinst",
InstanceType: PRODUCTION,
ClusterId: "mycluster",
Zone: "us-central1-a",
StorageType: SSD,
NodeScalingFactor: NodeScalingFactor1X,
AutoscalingConfig: &AutoscalingConfig{MinNodes: 1, MaxNodes: 2, CPUTargetPercent: 10, StorageUtilizationPerNode: 3000},
})
if err != nil {
t.Fatalf("CreateInstance failed: %v", err)
}
mycc := mock.createInstanceReq.Clusters["mycluster"]
cc := mycc.Config.(*btapb.Cluster_ClusterConfig_)
gotConfig := cc.ClusterConfig.ClusterAutoscalingConfig
wantMin := int32(1)
if gotMin := gotConfig.AutoscalingLimits.MinServeNodes; wantMin != gotMin {
t.Fatalf("want autoscaling min nodes = %v, got = %v", wantMin, gotMin)
}
wantMax := int32(2)
if gotMax := gotConfig.AutoscalingLimits.MaxServeNodes; wantMax != gotMax {
t.Fatalf("want autoscaling max nodes = %v, got = %v", wantMax, gotMax)
}
wantCPU := int32(10)
if gotCPU := gotConfig.AutoscalingTargets.CpuUtilizationPercent; wantCPU != gotCPU {
t.Fatalf("want autoscaling cpu = %v, got = %v", wantCPU, gotCPU)
}
err = c.CreateInstance(context.Background(), &InstanceConf{
InstanceId: "myinst",
DisplayName: "myinst",
InstanceType: PRODUCTION,
ClusterId: "mycluster",
Zone: "us-central1-a",
StorageType: SSD,
NodeScalingFactor: NodeScalingFactor1X,
NumNodes: 1,
})
if err != nil {
t.Fatalf("CreateInstance failed: %v", err)
}
// omitting autoscaling config results in a nil config in the request
mycc = mock.createInstanceReq.Clusters["mycluster"]
if cc := mycc.GetClusterConfig(); cc != nil {
t.Fatalf("want config = nil, got = %v", gotConfig)
}
}
func TestInstanceAdmin_CreateInstanceWithClusters_WithAutoscaling(t *testing.T) {
mock := &mockAdminClock{}
c := setupClient(t, mock)
err := c.CreateInstanceWithClusters(context.Background(), &InstanceWithClustersConfig{
InstanceID: "myinst",
DisplayName: "myinst",
InstanceType: PRODUCTION,
Clusters: []ClusterConfig{
{
ClusterID: "mycluster",
Zone: "us-central1-a",
StorageType: SSD,
NodeScalingFactor: NodeScalingFactor1X,
AutoscalingConfig: &AutoscalingConfig{MinNodes: 1, MaxNodes: 2, CPUTargetPercent: 10, StorageUtilizationPerNode: 3000},
},
},
})
if err != nil {
t.Fatalf("CreateInstanceWithClusters failed: %v", err)
}
mycc := mock.createInstanceReq.Clusters["mycluster"]
cc := mycc.Config.(*btapb.Cluster_ClusterConfig_)
gotConfig := cc.ClusterConfig.ClusterAutoscalingConfig
wantMin := int32(1)
if gotMin := gotConfig.AutoscalingLimits.MinServeNodes; wantMin != gotMin {
t.Fatalf("want autoscaling min nodes = %v, got = %v", wantMin, gotMin)
}
wantMax := int32(2)
if gotMax := gotConfig.AutoscalingLimits.MaxServeNodes; wantMax != gotMax {
t.Fatalf("want autoscaling max nodes = %v, got = %v", wantMax, gotMax)
}
wantCPU := int32(10)
if gotCPU := gotConfig.AutoscalingTargets.CpuUtilizationPercent; wantCPU != gotCPU {
t.Fatalf("want autoscaling cpu = %v, got = %v", wantCPU, gotCPU)
}
}
func TestInstanceAdmin_CreateCluster_WithAutoscaling(t *testing.T) {
mock := &mockAdminClock{}
c := setupClient(t, mock)
err := c.CreateCluster(context.Background(), &ClusterConfig{
ClusterID: "mycluster",
Zone: "us-central1-a",
StorageType: SSD,
AutoscalingConfig: &AutoscalingConfig{MinNodes: 1, MaxNodes: 2, CPUTargetPercent: 10, StorageUtilizationPerNode: 3000},
NodeScalingFactor: NodeScalingFactor1X,
})
if err != nil {
t.Fatalf("CreateCluster failed: %v", err)
}
cc := mock.createClusterReq.Cluster.Config.(*btapb.Cluster_ClusterConfig_)
gotConfig := cc.ClusterConfig.ClusterAutoscalingConfig
wantMin := int32(1)
if gotMin := gotConfig.AutoscalingLimits.MinServeNodes; wantMin != gotMin {
t.Fatalf("want autoscaling min nodes = %v, got = %v", wantMin, gotMin)
}
wantMax := int32(2)
if gotMax := gotConfig.AutoscalingLimits.MaxServeNodes; wantMax != gotMax {
t.Fatalf("want autoscaling max nodes = %v, got = %v", wantMax, gotMax)
}
wantCPU := int32(10)
if gotCPU := gotConfig.AutoscalingTargets.CpuUtilizationPercent; wantCPU != gotCPU {
t.Fatalf("want autoscaling cpu = %v, got = %v", wantCPU, gotCPU)
}
wantStorage := int32(3000)
if gotStorage := gotConfig.AutoscalingTargets.StorageUtilizationGibPerNode; wantStorage != gotStorage {
t.Fatalf("want autoscaling storage = %v, got = %v", wantStorage, gotStorage)
}
err = c.CreateCluster(context.Background(), &ClusterConfig{
ClusterID: "mycluster",
Zone: "us-central1-a",
StorageType: SSD,
NumNodes: 1,
NodeScalingFactor: NodeScalingFactor1X,
})
if err != nil {
t.Fatalf("CreateCluster failed: %v", err)
}
// omitting autoscaling config results in a nil config in the request
if cc := mock.createClusterReq.Cluster.GetClusterConfig(); cc != nil {
t.Fatalf("want config = nil, got = %v", gotConfig)
}
}
func TestInstanceAdmin_UpdateInstanceWithClusters_IgnoresInvalidClusters(t *testing.T) {
mock := &mockAdminClock{}
c := setupClient(t, mock)
err := c.UpdateInstanceWithClusters(context.Background(), &InstanceWithClustersConfig{
InstanceID: "myinst",
DisplayName: "myinst",
Clusters: []ClusterConfig{
{
ClusterID: "mycluster",
Zone: "us-central1-a",
// Cluster has no autoscaling or num nodes
// It should be ignored
},
},
})
if err != nil {
t.Fatalf("UpdateInstanceWithClusters failed: %v", err)
}
if mock.partialUpdateClusterReq != nil {
t.Fatalf("PartialUpdateCluster should not have been called, got = %v",
mock.partialUpdateClusterReq)
}
}
func TestInstanceAdmin_UpdateInstanceWithClusters_WithAutoscaling(t *testing.T) {
mock := &mockAdminClock{}
c := setupClient(t, mock)
err := c.UpdateInstanceWithClusters(context.Background(), &InstanceWithClustersConfig{
InstanceID: "myinst",
DisplayName: "myinst",
Clusters: []ClusterConfig{
{
ClusterID: "mycluster",
Zone: "us-central1-a",
AutoscalingConfig: &AutoscalingConfig{MinNodes: 1, MaxNodes: 2, CPUTargetPercent: 10, StorageUtilizationPerNode: 3000},
},
},
})
if err != nil {
t.Fatalf("UpdateInstanceWithClusters failed: %v", err)
}
cc := mock.partialUpdateClusterReq.Cluster.Config.(*btapb.Cluster_ClusterConfig_)
gotConfig := cc.ClusterConfig.ClusterAutoscalingConfig
wantMin := int32(1)
if gotMin := gotConfig.AutoscalingLimits.MinServeNodes; wantMin != gotMin {
t.Fatalf("want autoscaling min nodes = %v, got = %v", wantMin, gotMin)
}
wantMax := int32(2)
if gotMax := gotConfig.AutoscalingLimits.MaxServeNodes; wantMax != gotMax {
t.Fatalf("want autoscaling max nodes = %v, got = %v", wantMax, gotMax)
}
wantCPU := int32(10)
if gotCPU := gotConfig.AutoscalingTargets.CpuUtilizationPercent; wantCPU != gotCPU {
t.Fatalf("want autoscaling cpu = %v, got = %v", wantCPU, gotCPU)
}
wantStorage := int32(3000)
if gotStorage := gotConfig.AutoscalingTargets.StorageUtilizationGibPerNode; wantStorage != gotStorage {
t.Fatalf("want autoscaling storage = %v, got = %v", wantStorage, gotStorage)
}
err = c.UpdateInstanceWithClusters(context.Background(), &InstanceWithClustersConfig{
InstanceID: "myinst",
DisplayName: "myinst",
Clusters: []ClusterConfig{
{
ClusterID: "mycluster",
Zone: "us-central1-a",
NumNodes: 1,
// no autoscaling config
},
},
})
if err != nil {
t.Fatalf("UpdateInstanceWithClusters failed: %v", err)
}
got := mock.partialUpdateClusterReq.Cluster.Config
if got != nil {
t.Fatalf("want autoscaling config = nil, got = %v", gotConfig)
}
}
func TestInstanceAdmin_UpdateInstanceAndSyncClusters_WithAutoscaling(t *testing.T) {
mock := &mockAdminClock{
getClusterResp: &btapb.Cluster{
Name: ".../mycluster",
Location: ".../us-central1-a",
State: btapb.Cluster_READY,
DefaultStorageType: btapb.StorageType_SSD,
Config: &btapb.Cluster_ClusterConfig_{
ClusterConfig: &btapb.Cluster_ClusterConfig{
ClusterAutoscalingConfig: &btapb.Cluster_ClusterAutoscalingConfig{
AutoscalingLimits: &btapb.AutoscalingLimits{
MinServeNodes: 1,
MaxServeNodes: 2,
},
AutoscalingTargets: &btapb.AutoscalingTargets{
CpuUtilizationPercent: 10,
StorageUtilizationGibPerNode: 3000,
},
},
},
},
},
}
c := setupClient(t, mock)
_, err := UpdateInstanceAndSyncClusters(context.Background(), c, &InstanceWithClustersConfig{
InstanceID: "myinst",
DisplayName: "myinst",
Clusters: []ClusterConfig{
{
ClusterID: "mycluster",
Zone: "us-central1-a",
AutoscalingConfig: &AutoscalingConfig{MinNodes: 1, MaxNodes: 2, CPUTargetPercent: 10, StorageUtilizationPerNode: 3000},
},
},
})
if err != nil {
t.Fatalf("UpdateInstanceAndSyncClusters failed: %v", err)
}
cc := mock.partialUpdateClusterReq.Cluster.Config.(*btapb.Cluster_ClusterConfig_)
gotConfig := cc.ClusterConfig.ClusterAutoscalingConfig
wantMin := int32(1)
if gotMin := gotConfig.AutoscalingLimits.MinServeNodes; wantMin != gotMin {
t.Fatalf("want autoscaling min nodes = %v, got = %v", wantMin, gotMin)
}
wantMax := int32(2)
if gotMax := gotConfig.AutoscalingLimits.MaxServeNodes; wantMax != gotMax {
t.Fatalf("want autoscaling max nodes = %v, got = %v", wantMax, gotMax)
}
wantCPU := int32(10)
if gotCPU := gotConfig.AutoscalingTargets.CpuUtilizationPercent; wantCPU != gotCPU {
t.Fatalf("want autoscaling cpu = %v, got = %v", wantCPU, gotCPU)
}
wantStorage := int32(3000)
if gotStorage := gotConfig.AutoscalingTargets.StorageUtilizationGibPerNode; wantStorage != gotStorage {
t.Fatalf("want autoscaling storage = %v, got = %v", wantStorage, gotStorage)
}
_, err = UpdateInstanceAndSyncClusters(context.Background(), c, &InstanceWithClustersConfig{
InstanceID: "myinst",
DisplayName: "myinst",
Clusters: []ClusterConfig{
{
ClusterID: "mycluster",
Zone: "us-central1-a",
NumNodes: 1,
},
},
})
if err != nil {
t.Fatalf("UpdateInstanceAndSyncClusters failed: %v", err)
}
got := mock.partialUpdateClusterReq.Cluster.Config
if got != nil {
t.Fatalf("want autoscaling config = nil, got = %v", gotConfig)
}
}
func TestInstanceAdmin_CreateAppProfile(t *testing.T) {
mock := &mockAdminClock{}
c := setupClient(t, mock)
ctx := context.Background()
tests := []struct {
desc string
conf ProfileConf
wantProfile *btapb.AppProfile
wantErrMsg string
}{
{
desc: "SingleClusterRouting config and Deprecated SingleClusterRouting policy",
conf: ProfileConf{
ProfileID: "prof1",
InstanceID: "inst1",
Description: "desc1",
RoutingConfig: &SingleClusterRoutingConfig{
ClusterID: "cluster2",
AllowTransactionalWrites: false,
},
RoutingPolicy: SingleClusterRouting,
ClusterID: "cluster1",
AllowTransactionalWrites: true,
Isolation: &StandardIsolation{Priority: AppProfilePriorityHigh},
},
wantProfile: &btapb.AppProfile{
Description: "desc1",
RoutingPolicy: &btapb.AppProfile_SingleClusterRouting_{
SingleClusterRouting: &btapb.AppProfile_SingleClusterRouting{
ClusterId: "cluster2",
AllowTransactionalWrites: false,
},
},
Isolation: &btapb.AppProfile_StandardIsolation_{
StandardIsolation: &btapb.AppProfile_StandardIsolation{
Priority: btapb.AppProfile_PRIORITY_HIGH,
},
},
},
},
{
desc: "MultiClusterRoutingUseAny config with affinity and Deprecated SingleClusterRouting policy",
conf: ProfileConf{
ProfileID: "prof3",
InstanceID: "inst1",
Description: "desc3",
RoutingPolicy: SingleClusterRouting,
ClusterID: "cluster1",
AllowTransactionalWrites: true,
RoutingConfig: &MultiClusterRoutingUseAnyConfig{
ClusterIDs: []string{"cluster1"},
Affinity: &RowAffinity{},
},
},
wantProfile: &btapb.AppProfile{
Description: "desc3",
RoutingPolicy: &btapb.AppProfile_MultiClusterRoutingUseAny_{
MultiClusterRoutingUseAny: &btapb.AppProfile_MultiClusterRoutingUseAny{
ClusterIds: []string{"cluster1"},
Affinity: &btapb.AppProfile_MultiClusterRoutingUseAny_RowAffinity_{},
},
},
},
},
{
desc: "Error - No routing policy",
conf: ProfileConf{
ProfileID: "prof_err",
InstanceID: "inst1",
},
wantErrMsg: "bigtable: invalid RoutingPolicy",
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
mock.createAppProfileReq = nil // Reset mock request
_, gotErr := c.CreateAppProfile(ctx, tt.conf)
fmt.Println(gotErr)
if !((gotErr == nil && tt.wantErrMsg == "") ||
(gotErr != nil && tt.wantErrMsg != "" && strings.Contains(gotErr.Error(), tt.wantErrMsg))) {
t.Fatalf("CreateAppProfile error got: %v, want: %v", gotErr, tt.wantErrMsg)
}
if tt.wantErrMsg != "" {
return
}
// Verify the request sent to the mock
wantReq := &btapb.CreateAppProfileRequest{
Parent: "projects/my-cool-project/instances/inst1",
AppProfileId: tt.conf.ProfileID,
AppProfile: tt.wantProfile,
IgnoreWarnings: tt.conf.IgnoreWarnings,
}
// The name field is set by the server, so we don't compare it in the request's AppProfile
mock.createAppProfileReq.AppProfile.Name = ""
if diff := testutil.Diff(mock.createAppProfileReq, wantReq); diff != "" {
t.Errorf("CreateAppProfileRequest mismatch (-got +want):\n%s", diff)
}
})
}
}
func TestInstanceAdmin_UpdateAppProfile(t *testing.T) {
mock := &mockAdminClock{}
c := setupClient(t, mock)
ctx := context.Background()
tests := []struct {
desc string
profileID string
instanceID string
updateAttrs ProfileAttrsToUpdate
wantReq *btapb.UpdateAppProfileRequest
wantErr bool
}{
{
desc: "Update only routing to SingleClusterRoutingConfig",
profileID: "prof2",
instanceID: "inst1",
updateAttrs: ProfileAttrsToUpdate{
RoutingConfig: &SingleClusterRoutingConfig{
ClusterID: "cluster-new",
AllowTransactionalWrites: false,
},
},
wantReq: &btapb.UpdateAppProfileRequest{
AppProfile: &btapb.AppProfile{
Name: "projects/my-cool-project/instances/inst1/appProfiles/prof2",
RoutingPolicy: &btapb.AppProfile_SingleClusterRouting_{
SingleClusterRouting: &btapb.AppProfile_SingleClusterRouting{
ClusterId: "cluster-new",
AllowTransactionalWrites: false,
},
},
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"single_cluster_routing"}},
},
},
{
desc: "Update isolation to DataBoost",
profileID: "prof4",
instanceID: "inst1",
updateAttrs: ProfileAttrsToUpdate{
Isolation: &DataBoostIsolationReadOnly{ComputeBillingOwner: HostPays},
},
wantReq: &btapb.UpdateAppProfileRequest{
AppProfile: &btapb.AppProfile{
Name: "projects/my-cool-project/instances/inst1/appProfiles/prof4",
Isolation: &btapb.AppProfile_DataBoostIsolationReadOnly_{
DataBoostIsolationReadOnly: &btapb.AppProfile_DataBoostIsolationReadOnly{
ComputeBillingOwner: ptr(btapb.AppProfile_DataBoostIsolationReadOnly_HOST_PAYS),
},
},
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"data_boost_isolation_read_only"}},
},
},
{
desc: "Update isolation to DataBoost without ComputeBillingOwner",
profileID: "prof4",
instanceID: "inst1",
updateAttrs: ProfileAttrsToUpdate{
Isolation: &DataBoostIsolationReadOnly{},
},
wantReq: &btapb.UpdateAppProfileRequest{
AppProfile: &btapb.AppProfile{
Name: "projects/my-cool-project/instances/inst1/appProfiles/prof4",
Isolation: &btapb.AppProfile_DataBoostIsolationReadOnly_{
DataBoostIsolationReadOnly: &btapb.AppProfile_DataBoostIsolationReadOnly{
ComputeBillingOwner: ptr(btapb.AppProfile_DataBoostIsolationReadOnly_COMPUTE_BILLING_OWNER_UNSPECIFIED),
},
},
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"data_boost_isolation_read_only"}},
},
},
{
desc: "Update using both RoutingPolicy and MultiClusterRoutingUseAnyConfig",
profileID: "prof_dep",
instanceID: "inst1",
updateAttrs: ProfileAttrsToUpdate{
RoutingPolicy: optional.String(SingleClusterRouting),
RoutingConfig: &MultiClusterRoutingUseAnyConfig{
ClusterIDs: []string{"c1", "c2"},
Affinity: &RowAffinity{},
},
},
wantReq: &btapb.UpdateAppProfileRequest{
AppProfile: &btapb.AppProfile{
Name: "projects/my-cool-project/instances/inst1/appProfiles/prof_dep",
RoutingPolicy: &btapb.AppProfile_MultiClusterRoutingUseAny_{
MultiClusterRoutingUseAny: &btapb.AppProfile_MultiClusterRoutingUseAny{
ClusterIds: []string{"c1", "c2"},
Affinity: &btapb.AppProfile_MultiClusterRoutingUseAny_RowAffinity_{},
},
},
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"multi_cluster_routing_use_any"}},
},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
mock.updateAppProfileReq = nil // Reset mock request
err := c.UpdateAppProfile(ctx, tt.instanceID, tt.profileID, tt.updateAttrs)
if (err != nil) != tt.wantErr {
t.Fatalf("UpdateAppProfile error got: %v, want: %v", err, tt.wantErr)
}
// Verify the request sent to the mock
if diff := testutil.Diff(mock.updateAppProfileReq, tt.wantReq); diff != "" {
t.Errorf("UpdateAppProfileRequest mismatch (-got +want):\n%s", diff)
}
})
}
}