diff --git a/auth_subject.go b/auth_subject.go index 16d0bf6..aff9a16 100644 --- a/auth_subject.go +++ b/auth_subject.go @@ -1,9 +1,6 @@ package gcloudcx import ( - "context" - - "github.com/gildas/go-errors" "github.com/gildas/go-logger" "github.com/google/uuid" ) @@ -15,7 +12,21 @@ type AuthorizationSubject struct { Name string `json:"name"` Grants []AuthorizationGrant `json:"grants"` Version int `json:"version"` - Logger *logger.Logger `json:"-"` + logger *logger.Logger `json:"-"` +} + +// Initialize initializes the object +// +// accepted parameters: *gcloufcx.Client, *logger.Logger +// +// implements Initializable +func (subject *AuthorizationSubject) Initialize(parameters ...interface{}) { + for _, raw := range parameters { + switch parameter := raw.(type) { + case *logger.Logger: + subject.logger = parameter.Child("authorization_subject", "authorization_subject", "id", subject.ID) + } + } } // GetID gets the identifier @@ -25,31 +36,24 @@ func (subject AuthorizationSubject) GetID() uuid.UUID { return subject.ID } -func (subject *AuthorizationSubject) Fetch(context context.Context, client *Client, parameters ...interface{}) error { - id, name, selfURI, log := client.ParseParameters(context, subject, parameters...) - - if id != uuid.Nil { - if err := client.Get(context, NewURI("/authorization/subjects/%s", id), &subject); err != nil { - return err - } - } else if len(selfURI) > 0 { - if err := client.Get(context, selfURI, &subject); err != nil { - return err - } - } else if len(name) > 0 { - return errors.NotImplemented.WithStack() - } else { - return errors.CreationFailed.With("AuthorizationSubject") +// GetURI gets the URI of this +// +// implements Addressable +func (subject AuthorizationSubject) GetURI(ids ...uuid.UUID) URI { + if len(ids) > 0 { + return NewURI("/api/v2/authorization/subjects/%s", ids[0]) + } + if subject.ID != uuid.Nil { + return NewURI("/api/v2/authorization/subjects/%s", subject.ID) } - subject.Logger = log.Child("authorization_subject", "authorization_subject", "id", subject.ID) - return nil + return URI("/api/v2/authorization/subjects/") } // CheckScopes checks if the subject allows or denies the given scopes // // See https://developer.genesys.cloud/authorization/platform-auth/scopes#scope-descriptions func (subject AuthorizationSubject) CheckScopes(scopes ...string) (permitted []string, denied []string) { - log := subject.Logger.Child(nil, "check_scopes") + log := subject.logger.Child(nil, "check_scopes") for _, scope := range scopes { authScope := AuthorizationScope{}.With(scope) diff --git a/auth_test.go b/auth_test.go index 1f42109..a099bea 100644 --- a/auth_test.go +++ b/auth_test.go @@ -2,7 +2,10 @@ package gcloudcx_test import ( "context" + "encoding/json" "fmt" + "os" + "path/filepath" "reflect" "strings" "testing" @@ -29,11 +32,11 @@ func TestAuthSuite(t *testing.T) { suite.Run(t, new(AuthSuite)) } -// Suite Tools - +// ***************************************************************************** +// #region: Suite Tools {{{ func (suite *AuthSuite) SetupSuite() { _ = godotenv.Load() - suite.Name = strings.TrimSuffix(reflect.TypeOf(*suite).Name(), "Suite") + suite.Name = strings.TrimSuffix(reflect.TypeOf(suite).Elem().Name(), "Suite") suite.Logger = logger.Create("test", &logger.FileStream{ Path: fmt.Sprintf("./log/test-%s.log", strings.ToLower(suite.Name)), @@ -80,7 +83,7 @@ func (suite *AuthSuite) BeforeTest(suiteName, testName string) { if !suite.Client.IsAuthorized() { suite.Logger.Infof("Client is not logged in...") err := suite.Client.Login(context.Background()) - suite.Require().Nil(err, "Failed to login") + suite.Require().NoError(err, "Failed to login") suite.Logger.Infof("Client is now logged in...") } else { suite.Logger.Infof("Client is already logged in...") @@ -92,7 +95,20 @@ func (suite *AuthSuite) AfterTest(suiteName, testName string) { suite.Logger.Record("duration", duration.String()).Infof("Test End: %s %s", testName, strings.Repeat("-", 80-11-len(testName))) } -// Tests +func (suite *AuthSuite) LoadTestData(filename string) []byte { + data, err := os.ReadFile(filepath.Join(".", "testdata", filename)) + suite.Require().NoErrorf(err, "Failed to Load Data. %s", err) + return data +} + +func (suite *AuthSuite) UnmarshalData(filename string, v interface{}) error { + data := suite.LoadTestData(filename) + suite.Logger.Infof("Loaded %s: %s", filename, string(data)) + return json.Unmarshal(data, v) +} + +// #endregion: Suite Tools }}} +// ***************************************************************************** func (suite *AuthSuite) TestCanCreateAuthScopeFromString() { var scope gcloudcx.AuthorizationScope @@ -112,20 +128,21 @@ func (suite *AuthSuite) TestCanCreateAuthScopeFromString() { func (suite *AuthSuite) TestCanUnmarshalAuthorizationSubject() { subject := gcloudcx.AuthorizationSubject{} - err := LoadObject("authorization-subject.json", &subject) - suite.Require().NoError(err, "Failed to load authorization subject, Error: %s", err) + err := suite.UnmarshalData("authorization-subject.json", &subject) + suite.Require().NoErrorf(err, "Failed to load authorization subject, Error: %s", err) } func (suite *AuthSuite) TestCanUnmarshalAuthorizationSubjectWithDivisions() { subject := gcloudcx.AuthorizationSubject{} - err := LoadObject("authorization-subject-with-divisions.json", &subject) - suite.Require().NoError(err, "Failed to load authorization subject, Error: %s", err) + err := suite.UnmarshalData("authorization-subject-with-divisions.json", &subject) + suite.Require().NoErrorf(err, "Failed to load authorization subject, Error: %s", err) } func (suite *AuthSuite) TestCanCheckScopes() { subject := gcloudcx.AuthorizationSubject{} - err := LoadObject("authorization-subject.json", &subject) - suite.Require().NoError(err, "Failed to load authorization subject, Error: %s", err) + err := suite.UnmarshalData("authorization-subject.json", &subject) + suite.Require().NoErrorf(err, "Failed to load authorization subject, Error: %s", err) + subject.Initialize(suite.Logger) permitted, denied := subject.CheckScopes("routing:language:assign", "messaging:message", "processing:space:deploy") suite.Assert().Len(permitted, 2) suite.Assert().Len(denied, 1) diff --git a/client.go b/client.go index fa20f9e..614223a 100644 --- a/client.go +++ b/client.go @@ -95,79 +95,6 @@ func (client *Client) IsAuthorized() bool { return client.Grant.AccessToken().IsValid() } -// Fetch fetches an object from the Genesys Cloud API -// -// The object must implement the Fetchable interface -// -// Objects can be fetched by their ID: -// -// client.Fetch(context, &User{ID: uuid.UUID}) -// -// client.Fetch(context, &User{}, uuid.UUID) -// -// or by their name: -// -// client.Fetch(context, &User{}, "user-name") -// -// or by their URI: -// -// client.Fetch(context, &User{}, URI("/api/v2/users/user-id")) -// -// client.Fetch(context, &User{URI: "/api/v2/users/user-id"}) -func (client *Client) Fetch(ctx context.Context, object Fetchable, parameters ...interface{}) error { - if _, err := logger.FromContext(ctx); err != nil { - ctx = client.Logger.ToContext(ctx) - } - return object.Fetch(ctx, client, parameters...) -} - -// ParseParameters parses the parameters to get an id, name or URI -// -// the id can be a string or a uuid.UUID, or coming from the object -// -// the uri can be a URI, or coming from the object -// -// a logger.Logger is also returned, either from the context or the client -func (client *Client) ParseParameters(ctx context.Context, object interface{}, parameters ...interface{}) (uuid.UUID, string, URI, *logger.Logger) { - var ( - id uuid.UUID = uuid.Nil - name string - uri URI - ) - - for _, parameter := range parameters { - switch parameter := parameter.(type) { - case uuid.UUID: - id = parameter - case string: - name = parameter - case URI: - uri = parameter - default: - if identifiable, ok := parameter.(Identifiable); ok { - id = identifiable.GetID() - } - } - } - if identifiable, ok := object.(Identifiable); id == uuid.Nil && ok { - id = identifiable.GetID() - } - if addressable, ok := object.(Addressable); len(uri) == 0 && ok { - uri = addressable.GetURI() - } - log, err := logger.FromContext(ctx) - if err != nil { - log = client.Logger - } - if typed, ok := object.(core.TypeCarrier); ok { - log = log.Child(typed.GetType(), typed.GetType()) - } - if id != uuid.Nil { - log = log.Record("id", id.String()) - } - return id, name, uri, log -} - // CheckScopes checks if the current client allows/denies the given scopes // // See https://developer.genesys.cloud/authorization/platform-auth/scopes#scope-descriptions @@ -179,12 +106,11 @@ func (client *Client) CheckScopes(context context.Context, scopes ...string) (pe // // See https://developer.genesys.cloud/authorization/platform-auth/scopes#scope-descriptions func (client *Client) CheckScopesWithID(context context.Context, id core.Identifiable, scopes ...string) (permitted []string, denied []string, err error) { - var subject AuthorizationSubject - if id.GetID() == uuid.Nil { return nil, nil, errors.ArgumentMissing.With("id") } - if err := client.Fetch(context, &subject, id); err != nil { + subject, err := Fetch[AuthorizationSubject](context, client, id) + if err != nil { return []string{}, scopes, err } permitted, denied = subject.CheckScopes(scopes...) diff --git a/client_test.go b/client_test.go index 681090d..a1bbd07 100644 --- a/client_test.go +++ b/client_test.go @@ -25,27 +25,11 @@ func TestClientSuite(t *testing.T) { suite.Run(t, new(ClientSuite)) } -func (suite *ClientSuite) TestCanInitialize() { - client := gcloudcx.NewClient(&gcloudcx.ClientOptions{ - Region: "mypurecloud.com", - Logger: suite.Logger, - }).SetAuthorizationGrant(&gcloudcx.ClientCredentialsGrant{ - ClientID: uuid.New(), - Secret: "s3cr3t", - }) - suite.Require().NotNil(client, "gcloudcx Client is nil") -} - -func (suite *ClientSuite) TestCanInitializeWithoutOptions() { - client := gcloudcx.NewClient(nil) - suite.Require().NotNil(client, "GCloudCX Client is nil") -} - -// Suite Tools - +// ***************************************************************************** +// #region: Suite Tools {{{ func (suite *ClientSuite) SetupSuite() { _ = godotenv.Load() - suite.Name = strings.TrimSuffix(reflect.TypeOf(*suite).Name(), "Suite") + suite.Name = strings.TrimSuffix(reflect.TypeOf(suite).Elem().Name(), "Suite") suite.Logger = logger.Create("test", &logger.FileStream{ Path: fmt.Sprintf("./log/test-%s.log", strings.ToLower(suite.Name)), @@ -76,3 +60,22 @@ func (suite *ClientSuite) AfterTest(suiteName, testName string) { duration := time.Since(suite.Start) suite.Logger.Record("duration", duration.String()).Infof("Test End: %s %s", testName, strings.Repeat("-", 80-11-len(testName))) } + +// #endregion: Suite Tools }}} +// ***************************************************************************** + +func (suite *ClientSuite) TestCanInitialize() { + client := gcloudcx.NewClient(&gcloudcx.ClientOptions{ + Region: "mypurecloud.com", + Logger: suite.Logger, + }).SetAuthorizationGrant(&gcloudcx.ClientCredentialsGrant{ + ClientID: uuid.New(), + Secret: "s3cr3t", + }) + suite.Require().NotNil(client, "gcloudcx Client is nil") +} + +func (suite *ClientSuite) TestCanInitializeWithoutOptions() { + client := gcloudcx.NewClient(nil) + suite.Require().NotNil(client, "GCloudCX Client is nil") +} diff --git a/common_test.go b/common_test.go deleted file mode 100644 index 9810a31..0000000 --- a/common_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package gcloudcx_test - -import ( - "encoding/json" - "io/ioutil" - "path/filepath" -) - -func LoadObject(filename string, object interface{}) (err error) { - payload, err := LoadFile(filename) - if err != nil { - return err - } - if err = json.Unmarshal(payload, &object); err != nil { - return err - } - return nil -} - -func LoadFile(filename string) (payload []byte, err error) { - if payload, err = ioutil.ReadFile(filepath.Join(".", "testdata", filename)); err != nil { - return nil, err - } - return payload, nil -} diff --git a/conversation.go b/conversation.go index 440df51..882356b 100644 --- a/conversation.go +++ b/conversation.go @@ -77,41 +77,45 @@ type Voicemail struct { UploadStatus string `json:"uploadStatus"` } -// Fetch fetches the conversation +// Initialize initializes the object // -// implements Fetchable -func (conversation *Conversation) Fetch(ctx context.Context, client *Client, parameters ...interface{}) error { - id, _, selfURI, log := client.ParseParameters(ctx, conversation, parameters...) - - if id != uuid.Nil { - if err := client.Get(ctx, NewURI("/conversations/%s", id), &conversation); err != nil { - return err - } - conversation.logger = log - } else if len(selfURI) > 0 { - if err := client.Get(ctx, selfURI, &conversation); err != nil { - return err +// accepted parameters: *gcloufcx.Client, *logger.Logger +// +// implements Initializable +func (conversation *Conversation) Initialize(parameters ...interface{}) { + for _, raw := range parameters { + switch parameter := raw.(type) { + case *Client: + conversation.client = parameter + case *logger.Logger: + conversation.logger = parameter.Child("conversation", "conversation", "id", conversation.ID) } - conversation.logger = log.Record("id", conversation.ID) } - conversation.client = client - return nil } // GetID gets the identifier of this -// implements Identifiable +// +// implements Identifiable func (conversation Conversation) GetID() uuid.UUID { return conversation.ID } // GetURI gets the URI of this -// implements Addressable -func (conversation Conversation) GetURI() URI { - return conversation.SelfURI +// +// implements Addressable +func (conversation Conversation) GetURI(ids ...uuid.UUID) URI { + if len(ids) > 0 { + return NewURI("/api/v2/conversations/%s", ids[0]) + } + if conversation.ID != uuid.Nil { + return NewURI("/api/v2/conversations/%s", conversation.ID) + } + return URI("/api/v2/conversations/") } // String gets a string version -// implements the fmt.Stringer interface +// +// implements the fmt.Stringer interface func (conversation Conversation) String() string { if len(conversation.Name) != 0 { return conversation.Name @@ -120,7 +124,8 @@ func (conversation Conversation) String() string { } // Disconnect disconnect an Identifiable from this -// implements Disconnecter +// +// implements Disconnecter func (conversation Conversation) Disconnect(context context.Context, identifiable Identifiable) error { return conversation.client.Patch( conversation.logger.ToContext(context), @@ -131,7 +136,8 @@ func (conversation Conversation) Disconnect(context context.Context, identifiabl } // UpdateState update the state of an identifiable in this -// implements StateUpdater +// +// implements StateUpdater func (conversation Conversation) UpdateState(context context.Context, identifiable Identifiable, state string) error { return conversation.client.Patch( conversation.logger.ToContext(context), diff --git a/conversation_chat.go b/conversation_chat.go index 5cc459c..e4b584b 100644 --- a/conversation_chat.go +++ b/conversation_chat.go @@ -56,47 +56,52 @@ type JourneyContext struct { } `json:"triggeringAction"` } -// Fetch fetches a Chat Conversation +// Initialize initializes the object // -// implements Fetchable -func (conversation *ConversationChat) Fetch(ctx context.Context, client *Client, parameters ...interface{}) error { - id, _, selfURI, log := client.ParseParameters(ctx, conversation, parameters...) - - if id != uuid.Nil { - if err := client.Get(ctx, NewURI("/conversations/%s", id), &conversation); err != nil { - return err - } - conversation.logger = log.Record("media", "chat") - } else if len(selfURI) > 0 { - if err := client.Get(ctx, selfURI, &conversation); err != nil { - return err +// accepted parameters: *gcloufcx.Client, *logger.Logger +// +// implements Initializable +func (conversation *ConversationChat) Initialize(parameters ...interface{}) { + for _, raw := range parameters { + switch parameter := raw.(type) { + case *Client: + conversation.client = parameter + case *logger.Logger: + conversation.logger = parameter.Child("conversation", "conversation", "id", conversation.ID, "media", "chat") } - conversation.logger = log.Record("id", conversation.ID).Record("media", "chat") } - conversation.client = client - return nil } // GetID gets the identifier of this -// implements Identifiable +// +// implements Identifiable func (conversation ConversationChat) GetID() uuid.UUID { return conversation.ID } // GetURI gets the URI of this -// implements Addressable -func (conversation ConversationChat) GetURI() URI { - return conversation.SelfURI +// +// implements Addressable +func (conversation ConversationChat) GetURI(ids ...uuid.UUID) URI { + if len(ids) > 0 { + return NewURI("/api/v2/conversations/%s", ids[0]) + } + if conversation.ID != uuid.Nil { + return NewURI("/api/v2/conversations/%s", conversation.ID) + } + return URI("/api/v2/conversations/") } // String gets a string version -// implements the fmt.Stringer interface +// +// implements the fmt.Stringer interface func (conversation ConversationChat) String() string { return conversation.ID.String() } // Disconnect disconnect an Identifiable from this -// implements Disconnecter +// +// implements Disconnecter func (conversation ConversationChat) Disconnect(context context.Context, identifiable Identifiable) error { return conversation.client.Patch( conversation.logger.ToContext(context), @@ -107,7 +112,8 @@ func (conversation ConversationChat) Disconnect(context context.Context, identif } // UpdateState update the state of an identifiable in this -// implements StateUpdater +// +// implements StateUpdater func (conversation ConversationChat) UpdateState(context context.Context, identifiable Identifiable, state string) error { return conversation.client.Patch( conversation.logger.ToContext(context), @@ -118,7 +124,8 @@ func (conversation ConversationChat) UpdateState(context context.Context, identi } // Transfer transfers a participant of this Conversation to the given Queue -// implement Transferrer +// +// implement Transferrer func (conversation ConversationChat) Transfer(context context.Context, identifiable Identifiable, queue Identifiable) error { return conversation.client.Post( conversation.logger.ToContext(context), diff --git a/conversation_guestchat.go b/conversation_guestchat.go index e8f3cb3..e4e0f90 100644 --- a/conversation_guestchat.go +++ b/conversation_guestchat.go @@ -31,41 +31,72 @@ type ConversationGuestChat struct { logger *logger.Logger `json:"-"` } -// Fetch fetches a Conversation Guest Chat +// Initialize initializes the object // -// implements Fetchable -func (conversation *ConversationGuestChat) Fetch(ctx context.Context, client *Client, parameters ...interface{}) error { - id, name, selfURI, log := client.ParseParameters(ctx, conversation, parameters...) - - if id != uuid.Nil { - if err := client.Get(ctx, NewURI("/organizations/%s", id), &conversation); err != nil { - return err - } - conversation.logger = log - } else if len(selfURI) > 0 { - if err := client.Get(ctx, selfURI, &conversation); err != nil { - return err - } - conversation.logger = log.Record("id", conversation.ID) - } else if len(name) > 0 { - return errors.NotImplemented.WithStack() - } else { - if err := client.Get(ctx, NewURI("/organizations/me"), &conversation); err != nil { - return err +// accepted parameters: *gcloufcx.Client, *logger.Logger +// +// implements Initializable +func (conversation *ConversationGuestChat) Initialize(parameters ...interface{}) { + for _, raw := range parameters { + switch parameter := raw.(type) { + case *Client: + conversation.client = parameter + case *logger.Logger: + conversation.logger = parameter.Child("conversation", "conversation", "id", conversation.ID, "media", "guestchat") } - conversation.logger = log.Record("id", conversation.ID) } +} - guest := conversation.Guest - target := conversation.Target - for _, parameter := range parameters { - if paramGuest, ok := parameter.(*ChatMember); ok { - guest = paramGuest - } - if paramTarget, ok := parameter.(*RoutingTarget); ok { - target = paramTarget - } +// GetID gets the identifier of this +// implements Identifiable +func (conversation ConversationGuestChat) GetID() uuid.UUID { + return conversation.ID +} + +// GetURI gets the URI of this +// +// implements Addressable +func (conversation ConversationGuestChat) GetURI(ids ...uuid.UUID) URI { + if len(ids) > 0 { + return NewURI("/api/v2/conversations/%s", ids[0]) + } + if conversation.ID != uuid.Nil { + return NewURI("/api/v2/conversations/%s", conversation.ID) + } + return URI("/api/v2/conversations/") +} + +// String gets a string version +// implements the fmt.Stringer interface +func (conversation ConversationGuestChat) String() string { + return conversation.ID.String() +} + +// Connect connects a Guest Chat to its websocket and starts its message loop +// If the websocket was already connected, nothing happens +// If the environment variable PURECLOUD_LOG_HEARTBEAT is set to true, the Heartbeat topic will be logged +func (conversation *ConversationGuestChat) Connect(context context.Context) (err error) { + if conversation.Socket != nil { + return + } + conversation.Socket, _, err = websocket.DefaultDialer.Dial(conversation.EventStream, nil) + if err != nil { + _ = conversation.Close(context) + // return errors.NotConnectedError.With("Conversation") + return errors.NotConnected.Wrap(err) + } + go conversation.messageLoop() + return +} + +// Start starts a Conversation Guest Chat +func (conversation *ConversationGuestChat) Start(ctx context.Context, guest *ChatMember, target *RoutingTarget) error { + if conversation == nil || conversation.client == nil || conversation.logger == nil { + return errors.NotInitialized.With("Conversation") } + log := conversation.logger + client := conversation.client + if guest == nil { return errors.ArgumentMissing.With("Guest") } @@ -86,8 +117,8 @@ func (conversation *ConversationGuestChat) Fetch(ctx context.Context, client *Cl RoutingTarget *RoutingTarget `json:"routingTarget"` Guest *ChatMember `json:"memberInfo"` }{ - OrganizationID: client.Organization.ID.String(), - DeploymentID: client.DeploymentID.String(), + OrganizationID: conversation.client.Organization.ID.String(), + DeploymentID: conversation.client.DeploymentID.String(), RoutingTarget: target, Guest: guest, }, @@ -96,6 +127,7 @@ func (conversation *ConversationGuestChat) Fetch(ctx context.Context, client *Cl return err } + conversation.logger = log conversation.client = client conversation.Guest.DisplayName = guest.DisplayName conversation.Guest.AvatarURL = guest.AvatarURL @@ -109,41 +141,6 @@ func (conversation *ConversationGuestChat) Fetch(ctx context.Context, client *Cl return nil } -// GetID gets the identifier of this -// implements Identifiable -func (conversation ConversationGuestChat) GetID() uuid.UUID { - return conversation.ID -} - -// GetURI gets the URI of this -// implements Addressable -func (conversation ConversationGuestChat) GetURI() URI { - return conversation.SelfURI -} - -// String gets a string version -// implements the fmt.Stringer interface -func (conversation ConversationGuestChat) String() string { - return conversation.ID.String() -} - -// Connect connects a Guest Chat to its websocket and starts its message loop -// If the websocket was already connected, nothing happens -// If the environment variable PURECLOUD_LOG_HEARTBEAT is set to true, the Heartbeat topic will be logged -func (conversation *ConversationGuestChat) Connect(context context.Context) (err error) { - if conversation.Socket != nil { - return - } - conversation.Socket, _, err = websocket.DefaultDialer.Dial(conversation.EventStream, nil) - if err != nil { - _ = conversation.Close(context) - // return errors.NotConnectedError.With("Conversation") - return errors.NotConnected.Wrap(err) - } - go conversation.messageLoop() - return -} - // Close disconnects the websocket and the guest func (conversation *ConversationGuestChat) Close(context context.Context) (err error) { log := conversation.logger.Scope("close") diff --git a/entities.go b/entities.go new file mode 100644 index 0000000..affc450 --- /dev/null +++ b/entities.go @@ -0,0 +1,52 @@ +package gcloudcx + +import ( + "context" + "encoding/json" +) + +type Entities struct { + Entities [][]byte `json:"-"` + PageSize int64 `json:"pageSize"` + PageNumber int64 `json:"pageNumber"` + PageCount uint64 `json:"pageCount"` + PageTotal uint64 `json:"total"` + FirstURI string `json:"firstUri"` + SelfURI string `json:"selfUri"` + LastURI string `json:"lastUri"` +} + +func (client *Client) FetchEntities(context context.Context, uri URI) ([][]byte, error) { + entities := Entities{} + values := [][]byte{} + + page := uint64(1) + for { + if err := client.Get(context, uri.WithQuery(Query{"pageNumber": page}), &entities); err != nil { + return nil, err + } + values = append(values, entities.Entities...) + if page++; page > entities.PageCount { + break + } + } + return values, nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (entities *Entities) UnmarshalJSON(data []byte) error { + type surrogate Entities + var inner struct { + surrogate + Entities []json.RawMessage `json:"entities"` + } + if err := json.Unmarshal(data, &inner); err != nil { + return err + } + *entities = Entities(inner.surrogate) + entities.Entities = make([][]byte, 0, len(inner.Entities)) + for _, entity := range inner.Entities { + entities.Entities = append(entities.Entities, entity) + } + return nil +} diff --git a/error.go b/error.go index 8d4126a..ba1071e 100644 --- a/error.go +++ b/error.go @@ -3,6 +3,8 @@ package gcloudcx import ( "encoding/json" "fmt" + + "github.com/gildas/go-errors" ) var ( @@ -69,6 +71,7 @@ type APIError struct { CorrelationID string `json:"correlationId,omitempty"` Details []APIErrorDetails `json:"details,omitempty"` Errors []APIError `json:"errors,omitempty"` + Stack errors.StackTrace `json:"-"` } // APIErrorDetails contains the details of an APIError @@ -79,6 +82,12 @@ type APIErrorDetails struct { EntityName string `json:"entityName,omitempty"` } +// Clone creates an exact copy of this Error +func (e APIError) Clone() *APIError { + final := e + return &final +} + // Error returns a string representation of this error func (e APIError) Error() string { if len(e.MessageWithParams) > 0 { @@ -90,6 +99,52 @@ func (e APIError) Error() string { return e.Code } +// Is tells if this error matches the target. +// +// implements errors.Is interface (package "errors"). +// +// To check if an error is an errors.Error, simply write: +// if errors.Is(err, gcloudcx.APIError{}) { +// // do something with err +// } +func (e APIError) Is(target error) bool { + if actual, ok := target.(APIError); ok { + if len(actual.Code) == 0 { + return true // no ID means any error is a match + } + return e.Code == actual.Code + } + return false +} + +// As attempts to convert the given error into the given target +// +// As returns true if the conversion was successful and the target is now populated. +// +// Example: +// target := errors.ArgumentInvalid.Clone() +// if errors.As(err, &target) { +// // do something with target +// } +func (e APIError) As(target interface{}) bool { + if actual, ok := target.(**APIError); ok { + if *actual != nil && (*actual).Code != e.Code { + return false + } + copy := e + *actual = © + return true + } + return false +} + +// WithStack creates a new error from a given Error and records its stack. +func (e APIError) WithStack() error { + final := e + final.Stack.Initialize() + return final +} + // UnmarshalJSON decodes a JSON payload into an APIError func (e *APIError) UnmarshalJSON(payload []byte) (err error) { // Try to get an error from the login API (/oauth/token) diff --git a/examples/OpenMessaging/main.go b/examples/OpenMessaging/main.go index cf70334..ea4af02 100644 --- a/examples/OpenMessaging/main.go +++ b/examples/OpenMessaging/main.go @@ -52,6 +52,7 @@ func main() { port = flag.Int("port", core.GetEnvAsInt("PORT", 3000), "the port to listen to") wait = flag.Duration("graceful-timeout", time.Second*15, "the duration for which the server gracefully wait for existing connections to finish") + err error ) flag.Parse() @@ -87,7 +88,10 @@ func main() { // Initializing the OpenMessaging Integration config.Client.Logger.Infof("Fetching OpenMessaging Integration %s", *integrationName) - err := config.Client.Fetch(context.Background(), config.Integration, *integrationName) + match := func(integration gcloudcx.OpenMessagingIntegration) bool { + return integration.Name == *integrationName + } + config.Integration, err = gcloudcx.FetchBy(context.Background(), config.Client, match) if errors.Is(err, errors.NotFound) { Log.Infof("Creating a new OpenMessaging Integration for %s", *integrationName) diff --git a/examples/chat_bot/config.go b/examples/chat_bot/config.go index 2ffd524..c04ac97 100644 --- a/examples/chat_bot/config.go +++ b/examples/chat_bot/config.go @@ -73,21 +73,24 @@ func (config *AppConfig) Initialize(context context.Context, client *gcloudcx.Cl if config.AgentQueue == nil { return errors.ArgumentMissing.With("Queue") } + match := func(queuename string) func(queue gcloudcx.Queue) bool { + return func(queue gcloudcx.Queue) bool { + return strings.EqualFold(queue.Name, queuename) + } + } if len(config.AgentQueue.ID) == 0 { - queueName := config.AgentQueue.Name - config.AgentQueue, err = client.FindQueueByName(context, queueName) + config.AgentQueue, err = gcloudcx.FetchBy(context, client, match(config.AgentQueue.Name)) if err != nil { - return errors.Wrapf(err, "Failed to retrieve the Agent Queue %s", queueName) + return errors.Wrapf(err, "Failed to retrieve the Agent Queue %s", config.AgentQueue.Name) } } if config.BotQueue == nil { return errors.ArgumentMissing.With("Bot Queue") } if len(config.BotQueue.ID) == 0 { - queueName := config.BotQueue.Name - config.BotQueue, err = client.FindQueueByName(context, queueName) + config.BotQueue, err = gcloudcx.FetchBy(context, client, match(config.BotQueue.Name)) if err != nil { - return errors.Wrapf(err, "Failed to retrieve the Bot Queue %s", queueName) + return errors.Wrapf(err, "Failed to retrieve the Bot Queue %s", config.BotQueue.Name) } } diff --git a/examples/chat_bot/message_loop.go b/examples/chat_bot/message_loop.go index 1a2dc39..5dc26f3 100644 --- a/examples/chat_bot/message_loop.go +++ b/examples/chat_bot/message_loop.go @@ -82,12 +82,12 @@ func MessageLoop(config *AppConfig, client *gcloudcx.Client) { log.Infof("Conversation: %s, BodyType: %s, Body: %s, sender: %s", topic.Conversation, topic.BodyType, topic.Body, topic.Sender) if topic.Type == "message" && topic.BodyType == "standard" { // remove the noise... // We need a full conversation object, so we can operate on it - err := client.Fetch(context, topic.Conversation) + conversation, err := gcloudcx.Fetch[gcloudcx.ConversationChat](context, client, topic.Conversation) if err != nil { log.Errorf("Failed to retrieve a Conversation for ID %s", topic.Conversation, err) continue } - participant := findParticipant(topic.Conversation.Participants, config.User, "agent") + participant := findParticipant(conversation.Participants, config.User, "agent") if participant == nil { log.Debugf("%s is not one of the participants of this conversation", config.User) continue @@ -105,7 +105,7 @@ func MessageLoop(config *AppConfig, client *gcloudcx.Client) { } // Pretend the Chat Bot is typing... (whereis it is thinking... isn't it?) log.Record("chat", participant.Chats[0]).Debugf("The agent is now typing") - err = topic.Conversation.SetTyping(context, participant.Chats[0]) + err = conversation.SetTyping(context, participant.Chats[0]) if err != nil { log.Errorf("Failed to send Typing to Chat Member", err) } @@ -132,20 +132,20 @@ func MessageLoop(config *AppConfig, client *gcloudcx.Client) { continue } log.Record("response", response).Debugf("Received: %s", response.Fulfillment) - if err = topic.Conversation.Post(context, participant.Chats[0], response.Fulfillment); err != nil { + if err = conversation.Post(context, participant.Chats[0], response.Fulfillment); err != nil { log.Errorf("Failed to send Text to Chat Member", err) } switch { case response.EndConversation: log.Infof("Disconnecting Participant %s", participant) - if err := topic.Conversation.Disconnect(context, participant); err != nil { + if err := conversation.Disconnect(context, participant); err != nil { log.Errorf("Failed to Wrapup Participant %s", &participant, err) continue } case "agenttransfer" == strings.ToLower(response.Intent): log.Infof("Transferring Participant %s to Queue %s", participant, config.AgentQueue) log.Record("queue", config.AgentQueue).Debugf("Agent Queue: %s", config.AgentQueue) - if err := topic.Conversation.Transfer(context, participant, config.AgentQueue); err != nil { + if err := conversation.Transfer(context, participant, config.AgentQueue); err != nil { log.Errorf("Failed to Transfer Participant %s to Queue %s", &participant, config.AgentQueue, err) continue } diff --git a/fetch.go b/fetch.go new file mode 100644 index 0000000..b09f334 --- /dev/null +++ b/fetch.go @@ -0,0 +1,182 @@ +package gcloudcx + +import ( + "context" + "encoding/json" + + "github.com/gildas/go-errors" + "github.com/gildas/go-logger" + "github.com/google/uuid" +) + +// Fetch fetches an object from the Genesys Cloud API +// +// The object must implement the Fetchable interface +// +// Objects can be fetched by their ID: +// +// user, err := Fetch[gcloudcx.User](context, client, uuid.UUID) +// +// user, err := Fetch[gcloudcx.User](context, client, gcloudcx.User{ID: uuid.UUID}) +// +// or by their URI: +// +// user, err := Fetch[gcloudcx.User](context, client, gcloudcx.User{}.GetURI(uuid.UUID)) +func Fetch[T Fetchable, PT interface { + Initializable + *T +}](context context.Context, client *Client, parameters ...any) (*T, error) { + id, query, selfURI, log := parseFetchParameters(context, client, parameters...) + + if len(selfURI) > 0 { + var object T + if err := client.Get(context, selfURI.WithQuery(query), &object); err != nil { + return nil, err + } + PT(&object).Initialize(client, log) + return &object, nil + } + if id != uuid.Nil { + var object T + if err := client.Get(context, object.GetURI(id).WithQuery(query), &object); err != nil { + return nil, err + } + PT(&object).Initialize(client, log) + return &object, nil + } + return nil, errors.NotFound.WithStack() +} + +// FetchBy fetches an object from the Genesys Cloud API by a match function +// +// The object must implement the Fetchable interface +// +// match := func(user gcloudcx.User) bool { +// return user.Name == "John Doe" +// } +// user, err := FetchBy(context, client, match) +// +// A gcloudcx.Query can be added to narrow the request: +// +// user, err := FetchBy(context, client, match, gcloudcx.Query{Language: "en-US"}) +func FetchBy[T Fetchable, PT interface { + Initializable + *T +}](context context.Context, client *Client, match func(T) bool, parameters ...interface{}) (*T, error) { + if match == nil { + return nil, errors.ArgumentMissing.With("match function") + } + _, query, _, log := parseFetchParameters(context, client, parameters...) + entities := Entities{} + page := uint64(1) + var addressable T + for { + uri := addressable.GetURI().WithQuery(query).WithQuery(Query{"pageNumber": page}) + if err := client.Get(context, uri, &entities); err != nil { + return nil, err + } + for _, entity := range entities.Entities { + var object T + if err := json.Unmarshal(entity, &object); err == nil && match(object) { + PT(&object).Initialize(client, log) + return &object, nil + } + } + if page++; page > entities.PageCount { + break + } + } + return nil, errors.NotFound.WithStack() +} + +// FetchAll fetches all objects from the Genesys Cloud API +// +// The objects must implement the Fetchable interface +// +// users, err := FetchAll[gcloudcx.User](context, client) +// +// A gcloudcx.Query can be added to narrow the request: +// +// users, err := FetchAll[gcloudcx.User](context, client, gcloudcx.Query{Language: "en-US"}) +func FetchAll[T Fetchable, PT interface { + Initializable + *T +}](context context.Context, client *Client, parameters ...interface{}) ([]*T, error) { + _, query, _, log := parseFetchParameters(context, client, parameters...) + entities := Entities{} + objects := []*T{} + page := uint64(1) + var addressable T + for { + uri := addressable.GetURI().WithQuery(query).WithQuery(Query{"pageNumber": page}) + if err := client.Get(context, uri, &entities); err != nil { + return nil, err + } + for _, entity := range entities.Entities { + var object T + if err := json.Unmarshal(entity, &object); err == nil { + PT(&object).Initialize(client, log) + objects = append(objects, &object) + } + } + if page++; page > entities.PageCount { + break + } + } + return objects, nil +} + +/* +func (client *Client) FetchAll(context context.Context, object Addressable) ([]interface{}, error) { + entities := struct { + Entities []json.RawMessage `json:"entities"` + PageSize int `json:"pageSize"` + PageNumber int `json:"pageNumber"` + PageCount int `json:"pageCount"` + PageTotal int `json:"total"` + FirstURI string `json:"firstUri"` + SelfURI string `json:"selfUri"` + LastURI string `json:"lastUri"` + }{} + page := 1 + for { + uri := URI(addressable.GetURI().Base().String() + "?messengerType=" + messengerType + "&pageNumber=" + strconv.FormatUint(page, 10)) + if err := client.Get(context, uri, &entities); err != nil { + return nil, err + } + log.Record("response", entities).Infof("Got a response") + + } + return []interface{}{}, nil +} +*/ + +func parseFetchParameters(context context.Context, client *Client, parameters ...any) (uuid.UUID, Query, URI, *logger.Logger) { + var id uuid.UUID + var query Query + var uri URI + log, _ := logger.FromContext(context) + + if log == nil { + log = client.Logger + } + for _, parameter := range parameters { + switch parameter := parameter.(type) { + case uuid.UUID: + id = parameter + case Query: + query = parameter + case URI: + uri = parameter + case *logger.Logger: + log = parameter + default: + if identifiable, ok := parameter.(Identifiable); ok { + id = identifiable.GetID() + } else if addressable, ok := parameter.(Addressable); ok { + uri = addressable.GetURI() + } + } + } + return id, query, uri, log +} diff --git a/fetch_test.go b/fetch_test.go deleted file mode 100644 index 78eafa4..0000000 --- a/fetch_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package gcloudcx_test - -import ( - "context" - "fmt" - "reflect" - "strings" - "testing" - "time" - - "github.com/gildas/go-errors" - "github.com/gildas/go-gcloudcx" - "github.com/gildas/go-logger" - "github.com/google/uuid" - "github.com/stretchr/testify/suite" -) - -type FetchSuite struct { - suite.Suite - Name string - Logger *logger.Logger - Start time.Time -} - -func TestFetchSuite(t *testing.T) { - suite.Run(t, new(FetchSuite)) -} - -func (suite *FetchSuite) TestCanFetchObjectByID() { - client := gcloudcx.NewClient(&gcloudcx.ClientOptions{ - DeploymentID: uuid.New(), - Logger: suite.Logger, - }) - - stuff := Stuff{} - err := client.Fetch(context.Background(), &stuff, idFromGCloud) - suite.Require().Nil(err, "Failed to fetch stuff") -} - -func (suite *FetchSuite) TestCanFetchObjectByName() { - client := gcloudcx.NewClient(&gcloudcx.ClientOptions{ - DeploymentID: uuid.New(), - Logger: suite.Logger, - }) - - stuff := Stuff{} - err := client.Fetch(context.Background(), &stuff, nameFromGCloud) - suite.Require().Nil(err, "Failed to fetch stuff") -} - -func (suite *FetchSuite) TestShouldFailFetchingObjectWithUnknownID() { - client := gcloudcx.NewClient(&gcloudcx.ClientOptions{ - DeploymentID: uuid.New(), - Logger: suite.Logger, - }) - - stuff := Stuff{} - err := client.Fetch(context.Background(), &stuff, uuid.New()) - suite.Require().NotNil(err, "Failed to fetch stuff") - suite.Assert().ErrorIs(err, errors.NotFound) - // TODO: Check error has the unknown ID -} - -func (suite *FetchSuite) TestShouldFailFetchingObjectWithUnknownName() { - client := gcloudcx.NewClient(&gcloudcx.ClientOptions{ - DeploymentID: uuid.New(), - Logger: suite.Logger, - }) - - stuff := Stuff{} - err := client.Fetch(context.Background(), &stuff, "unknown") - suite.Require().NotNil(err, "Failed to fetch stuff") - suite.Assert().ErrorIs(err, errors.NotFound) - // TODO: Check error has the unknown name -} - -type Stuff struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - SelfURI gcloudcx.URI `json:"selfUri"` - client *gcloudcx.Client `json:"-"` - log *logger.Logger `json:"-"` -} - -var ( // simulate the fetching of the object via Genesys Cloud - idFromGCloud = uuid.MustParse("d4f8f8f8-f8f8-f8f8-f8f8-f8f8f8f8f8f8") - nameFromGCloud = "stuff" - uriFromGCloud = gcloudcx.URI(fmt.Sprintf("/api/v2/stuff/%s", idFromGCloud)) -) - -// GetID gets the identifier of this -// implements Identifiable -func (stuff Stuff) GetID() uuid.UUID { - return stuff.ID -} - -// GetURI gets the URI of this -// implements Addressable -func (stuff Stuff) GetURI() gcloudcx.URI { - return stuff.SelfURI -} - -func (stuff *Stuff) Fetch(ctx context.Context, client *gcloudcx.Client, parameters ...interface{}) error { - id, name, selfURI, log := client.ParseParameters(ctx, stuff, parameters...) - - if id != uuid.Nil { // Here, we should fetch the object from Genesys Cloud - if id != idFromGCloud { - return errors.NotFound.With("id", id.String()) - } - stuff.ID = idFromGCloud - stuff.Name = nameFromGCloud - stuff.SelfURI = uriFromGCloud - stuff.client = client - stuff.log = log - return nil - } - if len(name) > 0 { // Here, we should fetch the object from Genesys Cloud - if name != nameFromGCloud { - return errors.NotFound.With("name", name) - } - stuff.ID = idFromGCloud - stuff.Name = nameFromGCloud - stuff.SelfURI = uriFromGCloud - stuff.client = client - stuff.log = log - return nil - } - if len(selfURI) > 0 { // Here, we should fetch the object from Genesys Cloud - if selfURI != uriFromGCloud { - return errors.NotFound.With("selfURI", selfURI.String()) - } - stuff.ID = idFromGCloud - stuff.Name = nameFromGCloud - stuff.SelfURI = uriFromGCloud - stuff.client = client - stuff.log = log - return nil - } - return errors.NotFound.WithStack() -} - -// Suite Tools - -func (suite *FetchSuite) SetupSuite() { - suite.Name = strings.TrimSuffix(reflect.TypeOf(*suite).Name(), "Suite") - suite.Logger = logger.Create("test", - &logger.FileStream{ - Path: fmt.Sprintf("./log/test-%s.log", strings.ToLower(suite.Name)), - Unbuffered: true, - FilterLevel: logger.TRACE, - }, - ).Child("test", "test") - suite.Logger.Infof("Suite Start: %s %s", suite.Name, strings.Repeat("=", 80-14-len(suite.Name))) -} - -func (suite *FetchSuite) TearDownSuite() { - if suite.T().Failed() { - suite.Logger.Warnf("At least one test failed, we are not cleaning") - suite.T().Log("At least one test failed, we are not cleaning") - } else { - suite.Logger.Infof("All tests succeeded, we are cleaning") - } - suite.Logger.Infof("Suite End: %s %s", suite.Name, strings.Repeat("=", 80-12-len(suite.Name))) - suite.Logger.Close() -} - -func (suite *FetchSuite) BeforeTest(suiteName, testName string) { - suite.Logger.Infof("Test Start: %s %s", testName, strings.Repeat("-", 80-13-len(testName))) - suite.Start = time.Now() -} - -func (suite *FetchSuite) AfterTest(suiteName, testName string) { - duration := time.Since(suite.Start) - suite.Logger.Record("duration", duration.String()).Infof("Test End: %s %s", testName, strings.Repeat("-", 80-11-len(testName))) -} diff --git a/flow.go b/flow.go new file mode 100644 index 0000000..545b513 --- /dev/null +++ b/flow.go @@ -0,0 +1,65 @@ +package gcloudcx + +import ( + "encoding/json" + + "github.com/gildas/go-errors" + "github.com/google/uuid" +) + +type Flow struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Division *Division `json:"division,omitempty"` + IsActive bool `json:"active"` + IsSystem bool `json:"system"` + IsDeleted bool `json:"deleted"` +} + +// Initialize initializes the object +// +// implements Initializable +func (flow *Flow) Initialize(parameters ...interface{}) { +} + +// GetID gets the identifier of this +// implements Identifiable +func (flow Flow) GetID() uuid.UUID { + return flow.ID +} + +// GetURI gets the URI of this +// +// implements Addressable +func (flow Flow) GetURI(ids ...uuid.UUID) URI { + if len(ids) > 0 { + return NewURI("/api/v2/flows/%s", ids[0]) + } + if flow.ID != uuid.Nil { + return NewURI("/api/v2/flows/%s", flow.ID) + } + return URI("/api/v2/flows/") +} + +// String gets a string representation of this +// +// implements fmt.Stringer +func (flow Flow) String() string { + return flow.Name +} + +// MarshalJSON marshals this into JSON +// +// implements json.Marshaler +func (flow Flow) MarshalJSON() ([]byte, error) { + type surrogate Flow + data, err := json.Marshal(&struct { + surrogate + SelfURI URI `json:"selfUri"` + }{ + surrogate: surrogate(flow), + SelfURI: flow.GetURI(), + }) + return data, errors.JSONMarshalError.Wrap(err) +} diff --git a/go.mod b/go.mod index de09f92..97b57f2 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/gildas/go-gcloudcx -go 1.16 +go 1.18 require ( github.com/gildas/go-core v0.4.10 @@ -13,7 +13,30 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/joho/godotenv v1.4.0 github.com/matoous/go-nanoid/v2 v2.0.0 - github.com/stretchr/testify v1.7.1 + github.com/stretchr/testify v1.8.0 ) -replace github.com/gildas/go-errors => ../go-errors +require ( + cloud.google.com/go v0.104.0 // indirect + cloud.google.com/go/compute v1.10.0 // indirect + cloud.google.com/go/logging v1.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect + github.com/googleapis/gax-go/v2 v2.5.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.opencensus.io v0.23.0 // indirect + golang.org/x/net v0.0.0-20220923203811-8be639271d50 // indirect + golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 // indirect + golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 // indirect + golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/api v0.97.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20220923205249-dd2d53f1fffc // indirect + google.golang.org/grpc v1.49.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index 9b23968..9631301 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,10 @@ cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.0 h1:DAq3r8y4mDgyB/ZPJ9v/5VJNqjgJAxTn6ZYLlUywOu8= cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0 h1:gSmWO7DY1vOm0MVU6DNXM11BWHHsTUmsC5cv1fuW5X8= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -39,13 +41,16 @@ cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTB cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1 h1:2sMmt8prCn7DPaG4Pmh0N3Inmc8cT8ae5k1M6VJ9Wqc= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0 h1:aoLIYaA1fX3ywihqpBk2APQKOo20nXsp1GEZQbx5Jk4= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/logging v1.4.2 h1:Mu2Q75VBDQlW1HlBMjTX4X84UFR73G1TiLlRYc/b7tA= cloud.google.com/go/logging v1.4.2/go.mod h1:jco9QZSx8HiVVqLJReq7z7bVdj0P1Jb9PDFs63T+axo= +cloud.google.com/go/logging v1.5.0 h1:DcR52smaYLgeK9KPzJlBJyyBYqW/EGKiuRRl8boL1s4= +cloud.google.com/go/logging v1.5.0/go.mod h1:c/57U/aLdzSFuBtvbtFduG1Ii54uSm95HOBnp58P7/U= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -93,6 +98,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gildas/go-core v0.4.10 h1:xyitFfgAhzgALtb7Rasz2BljjFbTtvLAmzVljck7tyo= github.com/gildas/go-core v0.4.10/go.mod h1:M6pdgAkVsj7aQKyIuBFcT0dqW+9e5BstMgx0I+0CBDM= +github.com/gildas/go-errors v0.3.2 h1:5JRS+Y0sz3nVYHg4VB5n/AxPxnSlEJa6SJGiGap5P9M= +github.com/gildas/go-errors v0.3.2/go.mod h1:z1D8TK2JJG46uF3X8gAkE5W9useEXv71Ain9beY6re8= github.com/gildas/go-logger v1.5.5 h1:hrCa9qkYHSOsMcokNTov8ZJo0P64nFIQYOmDPhPbBTo= github.com/gildas/go-logger v1.5.5/go.mod h1:cY1CrMMBv5vcYwRqyzcmsQXhWihB9c8cUBdwc6W7I6M= github.com/gildas/go-request v0.7.9 h1:OJRc4PVsMHPPlJf938F/yMfriN20KGlnUSAaL43fBLw= @@ -149,8 +156,9 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -174,14 +182,18 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0 h1:zO8WHNx/MYiAKJ3d5spxZXZE6KHmIQGQcAzwUzV7qQw= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1 h1:kBRZU0PSuI7PspsSb/ChWoVResUcwNVIdpB049pKTiw= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= @@ -204,7 +216,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/matoous/go-nanoid v1.5.0 h1:VRorl6uCngneC4oUQqOYtO3S0H5QKFtKuKycFG3euek= github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0= github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g= @@ -215,12 +226,14 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -316,8 +329,12 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA= golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220923203811-8be639271d50 h1:vKyz8L3zkd+xrMeIaBsQ/MNVPVFSffdaU3ZyYlBGFnI= +golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -338,8 +355,11 @@ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 h1:zwrSfklXn0gxyLRX/aR+q6cgHbV/ItVyzbPlbA+dkAw= golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 h1:lxqLZaMad/dJHMFZH0NiNpiEZI/nhgWhe4wgzpE+MuA= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -352,8 +372,9 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 h1:ZrnxWX62AgTKOSagEqxvb3ffipvEDX2pl7E1TdqLqIc= +golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -412,8 +433,11 @@ golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc= +golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -486,6 +510,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -525,8 +550,10 @@ google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRR google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.82.0 h1:h6EGeZuzhoKSS7BUznzkW+2wHZ+4Ubd6rsVvvh3dRkw= google.golang.org/api v0.82.0/go.mod h1:Ld58BeTlL9DIYr2M2ajvoSqmGLei0BMn+kVBmkam1os= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.97.0 h1:x/vEL1XDF/2V4xzdNgFPaKHluRESo2aTsL7QzHnBtGQ= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -615,8 +642,13 @@ google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To= -google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 h1:qRu95HZ148xXw+XeZ3dvqe85PxH4X8+jIo0iRPKcEnM= google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220923205249-dd2d53f1fffc h1:saaNe2+SBQxandnzcD/qB1JEBQ2Pqew+KlFLLdA/XcM= +google.golang.org/genproto v0.0.0-20220923205249-dd2d53f1fffc/go.mod h1:yEEpwVWKMZZzo81NwRgyEJnA2fQvpXAYPVisv8EgDVs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -647,8 +679,9 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -663,8 +696,9 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/group.go b/group.go index 9ec7436..00ada07 100644 --- a/group.go +++ b/group.go @@ -1,10 +1,8 @@ package gcloudcx import ( - "context" "time" - "github.com/gildas/go-errors" "github.com/gildas/go-logger" "github.com/google/uuid" ) @@ -22,50 +20,52 @@ type Group struct { Images []*UserImage `json:"images"` Addresses []*Contact `json:"addresses"` RulesVisible bool `json:"rulesVisible"` - Visibility bool `json:"visibility"` + Visibility string `json:"visibility"` DateModified time.Time `json:"dateModified"` Version int `json:"version"` client *Client `json:"-"` logger *logger.Logger `json:"-"` } -// Fetch fetches a group +// Initialize initializes the object // -// implements Fetchable -func (group *Group) Fetch(ctx context.Context, client *Client, parameters ...interface{}) error { - id, name, selfURI, log := client.ParseParameters(ctx, group, parameters...) - - if id != uuid.Nil { - if err := client.Get(ctx, NewURI("/groups/%s", id), &group); err != nil { - return err - } - group.logger = log - } else if len(selfURI) > 0 { - if err := client.Get(ctx, selfURI, &group); err != nil { - return err +// accepted parameters: *gcloufcx.Client, *logger.Logger +// +// implements Initializable +func (group *Group) Initialize(parameters ...interface{}) { + for _, raw := range parameters { + switch parameter := raw.(type) { + case *Client: + group.client = parameter + case *logger.Logger: + group.logger = parameter.Child("group", "group", "id", group.ID) } - group.logger = log.Record("id", group.ID) - } else if len(name) > 0 { - return errors.NotImplemented.WithStack() } - group.client = client - return nil } // GetID gets the identifier of this -// implements Identifiable +// +// implements Identifiable func (group Group) GetID() uuid.UUID { return group.ID } // GetURI gets the URI of this -// implements Addressable -func (group Group) GetURI() URI { - return group.SelfURI +// +// implements Addressable +func (group Group) GetURI(ids ...uuid.UUID) URI { + if len(ids) > 0 { + return NewURI("/api/v2/groups/%s", ids[0]) + } + if group.ID != uuid.Nil { + return NewURI("/api/v2/groups/%s", group.ID) + } + return URI("/api/v2/groups/") } // String gets a string version -// implements the fmt.Stringer interface +// +// implements the fmt.Stringer interface func (group Group) String() string { if len(group.Name) > 0 { return group.Name diff --git a/group_test.go b/group_test.go index 4850d19..62b78cb 100644 --- a/group_test.go +++ b/group_test.go @@ -22,29 +22,22 @@ type GroupSuite struct { Logger *logger.Logger Start time.Time - Client *gcloudcx.Client + GroupID uuid.UUID + GroupName string + Client *gcloudcx.Client } func TestGroupSuite(t *testing.T) { suite.Run(t, new(GroupSuite)) } -func (suite *GroupSuite) TestCanStringify() { - id := uuid.New() - group := gcloudcx.Group{ - ID: id, - Name: "Hello", - } - suite.Assert().Equal("Hello", group.String()) - group.Name = "" - suite.Assert().Equal(id.String(), group.String()) -} - -// Suite Tools - +// ***************************************************************************** +// #region: Suite Tools {{{ func (suite *GroupSuite) SetupSuite() { + var err error + var value string _ = godotenv.Load() - suite.Name = strings.TrimSuffix(reflect.TypeOf(*suite).Name(), "Suite") + suite.Name = strings.TrimSuffix(reflect.TypeOf(suite).Elem().Name(), "Suite") suite.Logger = logger.Create("test", &logger.FileStream{ Path: fmt.Sprintf("./log/test-%s.log", strings.ToLower(suite.Name)), @@ -69,6 +62,16 @@ func (suite *GroupSuite) SetupSuite() { ClientID: clientID, Secret: secret, }) + + value = core.GetEnvAsString("GROUP_ID", "") + suite.Require().NotEmpty(value, "GROUP_ID is not set in your environment") + + suite.GroupID, err = uuid.Parse(value) + suite.Require().NoError(err, "GROUP_ID is not a valid UUID") + + suite.GroupName = core.GetEnvAsString("GROUP_NAME", "") + suite.Require().NotEmpty(suite.GroupName, "GROUP_NAME is not set in your environment") + suite.Require().NotNil(suite.Client, "GCloudCX Client is nil") } @@ -91,7 +94,7 @@ func (suite *GroupSuite) BeforeTest(suiteName, testName string) { if !suite.Client.IsAuthorized() { suite.Logger.Infof("Client is not logged in...") err := suite.Client.Login(context.Background()) - suite.Require().Nil(err, "Failed to login") + suite.Require().NoError(err, "Failed to login") suite.Logger.Infof("Client is now logged in...") } else { suite.Logger.Infof("Client is already logged in...") @@ -102,3 +105,36 @@ func (suite *GroupSuite) AfterTest(suiteName, testName string) { duration := time.Since(suite.Start) suite.Logger.Record("duration", duration.String()).Infof("Test End: %s %s", testName, strings.Repeat("-", 80-11-len(testName))) } + +// #endregion: Suite Tools }}} +// ***************************************************************************** + +func (suite *GroupSuite) TestCanFetchByID() { + group, err := gcloudcx.Fetch[gcloudcx.Group](context.Background(), suite.Client, suite.GroupID) + suite.Require().NoErrorf(err, "Failed to fetch Group %s. %s", suite.GroupID, err) + suite.Assert().Equal(suite.GroupID, group.ID) + suite.Assert().Equal(suite.GroupName, group.Name) + suite.Assert().Equal("public", group.Visibility) +} + +func (suite *GroupSuite) TestCanFetchByName() { + match := func(group gcloudcx.Group) bool { + return group.Name == suite.GroupName + } + group, err := gcloudcx.FetchBy(context.Background(), suite.Client, match) + suite.Require().NoErrorf(err, "Failed to fetch Group %s. %s", suite.GroupName, err) + suite.Assert().Equal(suite.GroupID, group.ID) + suite.Assert().Equal(suite.GroupName, group.Name) + suite.Assert().Equal("public", group.Visibility) +} + +func (suite *GroupSuite) TestCanStringify() { + id := uuid.New() + group := gcloudcx.Group{ + ID: id, + Name: "Hello", + } + suite.Assert().Equal("Hello", group.String()) + group.Name = "" + suite.Assert().Equal(id.String(), group.String()) +} diff --git a/login_test.go b/login_test.go index baf53db..a966124 100644 --- a/login_test.go +++ b/login_test.go @@ -34,57 +34,11 @@ func TestLoginSuite(t *testing.T) { suite.Run(t, new(LoginSuite)) } -func (suite *LoginSuite) TestCanLogin() { - err := suite.Client.SetAuthorizationGrant(&gcloudcx.ClientCredentialsGrant{ - ClientID: uuid.MustParse(core.GetEnvAsString("PURECLOUD_CLIENTID", "")), - Secret: core.GetEnvAsString("PURECLOUD_CLIENTSECRET", ""), - }).Login(context.Background()) - suite.Assert().Nil(err, "Failed to login") -} - -func (suite *LoginSuite) TestFailsLoginWithInvalidClientID() { - err := suite.Client.LoginWithAuthorizationGrant(context.Background(), &gcloudcx.ClientCredentialsGrant{ - ClientID: uuid.New(), // that UUID should not be anywhere in GCloud - Secret: core.GetEnvAsString("PURECLOUD_CLIENTSECRET", ""), - }) - suite.Assert().NotNil(err, "Should have failed login in") - - var apierr gcloudcx.APIError - ok := errors.As(err, &apierr) - suite.Require().Truef(ok, "Error is not a gcloudcx.APIError, error: %+v", err) - suite.Logger.Record("apierr", apierr).Errorf("API Error", err) - suite.Assert().Equal(errors.HTTPBadRequest.Code, apierr.Status) - suite.Assert().Equal("client not found: invalid_client", apierr.Error()) -} - -func (suite *LoginSuite) TestFailsLoginWithInvalidSecret() { - err := suite.Client.LoginWithAuthorizationGrant(context.Background(), &gcloudcx.ClientCredentialsGrant{ - ClientID: uuid.MustParse(core.GetEnvAsString("PURECLOUD_CLIENTID", "")), - Secret: "WRONGSECRET", - }) - suite.Assert().NotNil(err, "Should have failed login in") - - var apierr gcloudcx.APIError - ok := errors.As(err, &apierr) - suite.Require().Truef(ok, "Error is not a gcloudcx.APIError, error: %+v", err) - suite.Logger.Record("apierr", apierr).Errorf("API Error", err) - suite.Assert().Equal(errors.HTTPUnauthorized.Code, apierr.Status) - suite.Assert().Equal("authentication failed: invalid_client", apierr.Error()) -} - -func (suite *LoginSuite) TestCanLoginWithClientCredentialsGrant() { - err := suite.Client.LoginWithAuthorizationGrant(context.Background(), &gcloudcx.ClientCredentialsGrant{ - ClientID: uuid.MustParse(core.GetEnvAsString("PURECLOUD_CLIENTID", "")), - Secret: core.GetEnvAsString("PURECLOUD_CLIENTSECRET", ""), - }) - suite.Assert().Nil(err, "Failed to login") -} - -// Suite Tools - +// ***************************************************************************** +// #region: Suite Tools {{{ func (suite *LoginSuite) SetupSuite() { _ = godotenv.Load() - suite.Name = strings.TrimSuffix(reflect.TypeOf(*suite).Name(), "Suite") + suite.Name = strings.TrimSuffix(reflect.TypeOf(suite).Elem().Name(), "Suite") suite.Logger = logger.Create("test", &logger.FileStream{ Path: fmt.Sprintf("./log/test-%s.log", strings.ToLower(suite.Name)), @@ -127,3 +81,52 @@ func (suite *LoginSuite) AfterTest(suiteName, testName string) { duration := time.Since(suite.Start) suite.Logger.Record("duration", duration.String()).Infof("Test End: %s %s", testName, strings.Repeat("-", 80-11-len(testName))) } + +// #endregion: Suite Tools }}} +// ***************************************************************************** + +func (suite *LoginSuite) TestCanLogin() { + err := suite.Client.SetAuthorizationGrant(&gcloudcx.ClientCredentialsGrant{ + ClientID: uuid.MustParse(core.GetEnvAsString("PURECLOUD_CLIENTID", "")), + Secret: core.GetEnvAsString("PURECLOUD_CLIENTSECRET", ""), + }).Login(context.Background()) + suite.Assert().NoError(err, "Failed to login") +} + +func (suite *LoginSuite) TestFailsLoginWithInvalidClientID() { + err := suite.Client.LoginWithAuthorizationGrant(context.Background(), &gcloudcx.ClientCredentialsGrant{ + ClientID: uuid.New(), // that UUID should not be anywhere in GCloud + Secret: core.GetEnvAsString("PURECLOUD_CLIENTSECRET", ""), + }) + suite.Assert().Error(err, "Should have failed login in") + + var apierr gcloudcx.APIError + ok := errors.As(err, &apierr) + suite.Require().Truef(ok, "Error is not a gcloudcx.APIError, error: %+v", err) + suite.Logger.Record("apierr", apierr).Errorf("API Error", err) + suite.Assert().Equal(errors.HTTPBadRequest.Code, apierr.Status) + suite.Assert().Equal("client not found: invalid_client", apierr.Error()) +} + +func (suite *LoginSuite) TestFailsLoginWithInvalidSecret() { + err := suite.Client.LoginWithAuthorizationGrant(context.Background(), &gcloudcx.ClientCredentialsGrant{ + ClientID: uuid.MustParse(core.GetEnvAsString("PURECLOUD_CLIENTID", "")), + Secret: "WRONGSECRET", + }) + suite.Assert().Error(err, "Should have failed login in") + + var apierr gcloudcx.APIError + ok := errors.As(err, &apierr) + suite.Require().Truef(ok, "Error is not a gcloudcx.APIError, error: %+v", err) + suite.Logger.Record("apierr", apierr).Errorf("API Error", err) + suite.Assert().Equal(errors.HTTPUnauthorized.Code, apierr.Status) + suite.Assert().Equal("authentication failed: invalid_client", apierr.Error()) +} + +func (suite *LoginSuite) TestCanLoginWithClientCredentialsGrant() { + err := suite.Client.LoginWithAuthorizationGrant(context.Background(), &gcloudcx.ClientCredentialsGrant{ + ClientID: uuid.MustParse(core.GetEnvAsString("PURECLOUD_CLIENTID", "")), + Secret: core.GetEnvAsString("PURECLOUD_CLIENTSECRET", ""), + }) + suite.Assert().NoError(err, "Failed to login") +} diff --git a/openmessaging_integration.go b/openmessaging_integration.go index 4b45343..aae585a 100644 --- a/openmessaging_integration.go +++ b/openmessaging_integration.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "net/url" - "strings" "time" "github.com/gildas/go-core" @@ -31,23 +30,10 @@ type OpenMessagingIntegration struct { CreateStatus string `json:"createStatus,omitempty"` // Initiated, Completed, Error CreateError *ErrorBody `json:"createError,omitempty"` Status string `json:"status,omitempty"` // Active, Inactive - SelfURI URI `json:"selfUri,omitempty"` client *Client `json:"-"` logger *logger.Logger `json:"-"` } -// GetID gets the identifier of this -// implements Identifiable -func (integration OpenMessagingIntegration) GetID() uuid.UUID { - return integration.ID -} - -// GetURI gets the URI of this -// implements Addressable -func (integration OpenMessagingIntegration) GetURI() URI { - return integration.SelfURI -} - // IsCreated tells if this OpenMessagingIntegration has been created successfully func (integration OpenMessagingIntegration) IsCreated() bool { return integration.CreateStatus == "Completed" @@ -58,77 +44,40 @@ func (integration OpenMessagingIntegration) IsError() bool { return integration.CreateStatus == "Error" } -// Fetch fetches an OpenMessaging Integration +// Initialize initializes the object // -// implements Fetchable -func (integration *OpenMessagingIntegration) Fetch(ctx context.Context, client *Client, parameters ...interface{}) error { - id, name, selfURI, log := client.ParseParameters(ctx, integration, parameters...) - - if id != uuid.Nil { - if err := client.Get(ctx, NewURI("/conversations/messaging/integrations/open/%s", id), &integration); err != nil { - return err - } - integration.logger = log - } else if len(selfURI) > 0 { - if err := client.Get(ctx, selfURI, &integration); err != nil { - return err - } - integration.logger = log.Record("id", integration.ID) - } else if len(name) > 0 { - response := struct { - Integrations []*OpenMessagingIntegration `json:"entities"` - PageSize int `json:"pageSize"` - PageNumber int `json:"pageNumber"` - PageCount int `json:"pageCount"` - PageTotal int `json:"total"` - FirstURI string `json:"firstUri"` - SelfURI string `json:"selfUri"` - LastURI string `json:"lastUri"` - }{} - if err := client.Get(ctx, "/conversations/messaging/integrations/open", &response); err != nil { - return err - } - nameLowercase := strings.ToLower(name) - for _, item := range response.Integrations { - if strings.Compare(strings.ToLower(item.Name), nameLowercase) == 0 { - *integration = *item - break - } - } - if integration == nil || integration.ID == uuid.Nil { - return errors.NotFound.With("name", name) +// accepted parameters: *gcloufcx.Client, *logger.Logger +// +// implements Initializable +func (user *OpenMessagingIntegration) Initialize(parameters ...interface{}) { + for _, raw := range parameters { + switch parameter := raw.(type) { + case *Client: + user.client = parameter + case *logger.Logger: + user.logger = parameter.Child("user", "user", "id", user.ID) } - integration.logger = log.Record("id", integration.ID) - } else { - return errors.ArgumentMissing.With("idOrName") } - integration.client = client - return nil } -// FetchOpenMessagingIntegrations Fetches all OpenMessagingIntegration object -func (client *Client) FetchOpenMessagingIntegrations(ctx context.Context, parameters ...interface{}) ([]*OpenMessagingIntegration, error) { - _, _, _, log := client.ParseParameters(ctx, nil, parameters...) - entities := struct { - Integrations []*OpenMessagingIntegration `json:"entities"` - PageSize int `json:"pageSize"` - PageNumber int `json:"pageNumber"` - PageCount int `json:"pageCount"` - PageTotal int `json:"total"` - FirstURI string `json:"firstUri"` - SelfURI string `json:"selfUri"` - LastURI string `json:"lastUri"` - }{} - if err := client.Get(ctx, "/conversations/messaging/integrations/open", &entities); err != nil { - return nil, err +// GetID gets the identifier of this +// +// implements Identifiable +func (integration OpenMessagingIntegration) GetID() uuid.UUID { + return integration.ID +} + +// GetURI gets the URI of this +// +// implements Addressable +func (integration OpenMessagingIntegration) GetURI(ids ...uuid.UUID) URI { + if len(ids) > 0 { + return NewURI("/api/v2/conversations/messaging/integrations/open/%s", ids[0]) } - log.Record("response", entities).Infof("Got a response") - for _, integration := range entities.Integrations { - integration.client = client - integration.logger = log.Child("openmessagingintegration", "openmessagingintegration", "id", integration.ID) + if integration.ID != uuid.Nil { + return NewURI("/api/v2/conversations/messaging/integrations/open/%s", integration.ID) } - // TODO: fetch all pages!!! - return entities.Integrations, nil + return URI("/api/v2/conversations/messaging/integrations/open/") } // Create creates a new OpenMessaging Integration @@ -174,7 +123,7 @@ func (integration *OpenMessagingIntegration) Delete(context context.Context) err func (integration *OpenMessagingIntegration) Refresh(ctx context.Context) error { var value OpenMessagingIntegration - if err := integration.client.Get(ctx, integration.SelfURI, &value); err != nil { + if err := integration.client.Get(ctx, integration.GetURI(), &value); err != nil { return err } integration.Name = value.Name @@ -219,6 +168,17 @@ func (integration *OpenMessagingIntegration) Update(context context.Context, nam return nil } +// GetRoutingMessageRecipient fetches the RoutingMessageRecipient for this OpenMessagingIntegration +func (integration *OpenMessagingIntegration) GetRoutingMessageRecipient(context context.Context) (*RoutingMessageRecipient, error) { + if integration == nil || integration.ID == uuid.Nil { + return nil, errors.ArgumentMissing.With("ID") + } + if !integration.IsCreated() { + return nil, errors.CreationFailed.With("integration", integration.ID) + } + return Fetch[RoutingMessageRecipient](context, integration.client, integration) +} + // SendInboundMessage sends a text message from the middleware to GENESYS Cloud // // See https://developer.genesys.cloud/api/digital/openmessaging/inboundMessages#send-an-inbound-open-message @@ -386,10 +346,12 @@ func (integration OpenMessagingIntegration) MarshalJSON() ([]byte, error) { type surrogate OpenMessagingIntegration data, err := json.Marshal(struct { surrogate - W *core.URL `json:"outboundNotificationWebhookUrl"` + WebhookURL *core.URL `json:"outboundNotificationWebhookUrl"` + SelfURI URI `json:"selfUri"` }{ - surrogate: surrogate(integration), - W: (*core.URL)(integration.WebhookURL), + surrogate: surrogate(integration), + WebhookURL: (*core.URL)(integration.WebhookURL), + SelfURI: integration.GetURI(), }) return data, errors.JSONMarshalError.Wrap(err) } @@ -399,14 +361,14 @@ func (integration *OpenMessagingIntegration) UnmarshalJSON(payload []byte) (err type surrogate OpenMessagingIntegration var inner struct { surrogate - W *core.URL `json:"outboundNotificationWebhookUrl"` + WebhookURL *core.URL `json:"outboundNotificationWebhookUrl"` } if err = json.Unmarshal(payload, &inner); err != nil { return errors.JSONUnmarshalError.Wrap(err) } *integration = OpenMessagingIntegration(inner.surrogate) - integration.WebhookURL = (*url.URL)(inner.W) + integration.WebhookURL = (*url.URL)(inner.WebhookURL) return } diff --git a/openmessaging_test.go b/openmessaging_test.go index 683a308..df1b31d 100644 --- a/openmessaging_test.go +++ b/openmessaging_test.go @@ -5,12 +5,15 @@ import ( "encoding/json" "fmt" "net/url" + "os" + "path/filepath" "reflect" "strings" "testing" "time" "github.com/gildas/go-core" + "github.com/gildas/go-errors" "github.com/gildas/go-logger" "github.com/google/uuid" "github.com/joho/godotenv" @@ -25,33 +28,104 @@ type OpenMessagingSuite struct { Logger *logger.Logger Start time.Time - IntegrationID uuid.UUID - Client *gcloudcx.Client + IntegrationID uuid.UUID + IntegrationName string + Client *gcloudcx.Client } func TestOpenMessagingSuite(t *testing.T) { suite.Run(t, new(OpenMessagingSuite)) } -func (suite *OpenMessagingSuite) TestCanFetchIntegrations() { - integrations, err := suite.Client.FetchOpenMessagingIntegrations(context.Background()) - suite.Require().Nil(err, "Failed to fetch OpenMessaging Integrations") - if len(integrations) > 0 { - for _, integration := range integrations { - suite.Logger.Record("integration", integration).Infof("Got a integration") - suite.Assert().NotEmpty(integration.ID) - suite.Assert().NotEmpty(integration.Name) - suite.Assert().NotNil(integration.WebhookURL, "WebhookURL should not be nil (%s)", integration.Name) - } +// ***************************************************************************** +// #region: Suite Tools {{{ +func (suite *OpenMessagingSuite) SetupSuite() { + _ = godotenv.Load() + suite.Name = strings.TrimSuffix(reflect.TypeOf(suite).Elem().Name(), "Suite") + suite.Logger = logger.Create("test", + &logger.FileStream{ + Path: fmt.Sprintf("./log/test-%s.log", strings.ToLower(suite.Name)), + Unbuffered: true, + FilterLevel: logger.TRACE, + }, + ).Child("test", "test") + suite.Logger.Infof("Suite Start: %s %s", suite.Name, strings.Repeat("=", 80-14-len(suite.Name))) + + var ( + region = core.GetEnvAsString("PURECLOUD_REGION", "") + clientID = uuid.MustParse(core.GetEnvAsString("PURECLOUD_CLIENTID", "")) + secret = core.GetEnvAsString("PURECLOUD_CLIENTSECRET", "") + token = core.GetEnvAsString("PURECLOUD_CLIENTTOKEN", "") + deploymentID = uuid.MustParse(core.GetEnvAsString("PURECLOUD_DEPLOYMENTID", "")) + ) + suite.Client = gcloudcx.NewClient(&gcloudcx.ClientOptions{ + Region: region, + DeploymentID: deploymentID, + Logger: suite.Logger, + }).SetAuthorizationGrant(&gcloudcx.ClientCredentialsGrant{ + ClientID: clientID, + Secret: secret, + Token: gcloudcx.AccessToken{ + Type: "bearer", + Token: token, + }, + }) + suite.IntegrationName = "TEST-GO-PURECLOUD" + suite.Require().NotNil(suite.Client, "GCloudCX Client is nil") +} + +func (suite *OpenMessagingSuite) TearDownSuite() { + if suite.T().Failed() { + suite.Logger.Warnf("At least one test failed, we are not cleaning") + suite.T().Log("At least one test failed, we are not cleaning") + } else { + suite.Logger.Infof("All tests succeeded, we are cleaning") } + suite.Logger.Infof("Suite End: %s %s", suite.Name, strings.Repeat("=", 80-12-len(suite.Name))) + suite.Logger.Close() } -func (suite *OpenMessagingSuite) TestCanCreateIntegration() { - name := "TEST-GO-PURECLOUD" +func (suite *OpenMessagingSuite) BeforeTest(suiteName, testName string) { + suite.Logger.Infof("Test Start: %s %s", testName, strings.Repeat("-", 80-13-len(testName))) + + suite.Start = time.Now() + + // Reuse tokens as much as we can + if !suite.Client.IsAuthorized() { + suite.Logger.Infof("Client is not logged in...") + err := suite.Client.Login(context.Background()) + suite.Require().NoError(err, "Failed to login") + suite.Logger.Infof("Client is now logged in...") + } else { + suite.Logger.Infof("Client is already logged in...") + } +} + +func (suite *OpenMessagingSuite) AfterTest(suiteName, testName string) { + duration := time.Since(suite.Start) + suite.Logger.Record("duration", duration.String()).Infof("Test End: %s %s", testName, strings.Repeat("-", 80-11-len(testName))) +} + +func (suite *OpenMessagingSuite) LoadTestData(filename string) []byte { + data, err := os.ReadFile(filepath.Join(".", "testdata", filename)) + suite.Require().NoErrorf(err, "Failed to Load Data. %s", err) + return data +} + +func (suite *OpenMessagingSuite) UnmarshalData(filename string, v interface{}) error { + data := suite.LoadTestData(filename) + suite.Logger.Infof("Loaded %s: %s", filename, string(data)) + return json.Unmarshal(data, v) +} + +// #endregion: Suite Tools }}} +// ***************************************************************************** + +func (suite *OpenMessagingSuite) TestCan00CreateIntegration() { webhookURL, _ := url.Parse("https://www.genesys.com/gcloudcx") webhookToken := "DEADBEEF" - integration, err := suite.Client.CreateOpenMessagingIntegration(context.Background(), name, webhookURL, webhookToken, nil) - suite.Require().Nil(err, "Failed to create integration") + integration, err := suite.Client.CreateOpenMessagingIntegration(context.Background(), suite.IntegrationName, webhookURL, webhookToken, nil) + suite.Require().NoError(err, "Failed to create integration") suite.Logger.Record("integration", integration).Infof("Created a integration") for { if integration.IsCreated() { @@ -60,32 +134,65 @@ func (suite *OpenMessagingSuite) TestCanCreateIntegration() { suite.Logger.Warnf("Integration %s is still in status: %s, waiting a bit", integration.ID, integration.CreateStatus) time.Sleep(time.Second) err = integration.Refresh(context.Background()) - suite.Require().Nil(err, "Failed to refresh integration") + suite.Require().NoError(err, "Failed to refresh integration") } suite.IntegrationID = integration.ID } -func (suite *OpenMessagingSuite) TestCanDeleteIntegration() { +func (suite *OpenMessagingSuite) TestCanFetchByID() { + integration, err := gcloudcx.Fetch[gcloudcx.OpenMessagingIntegration](context.Background(), suite.Client, suite.IntegrationID) + suite.Require().NoErrorf(err, "Failed to fetch Open Messaging Integration %s. %s", suite.IntegrationID, err) + suite.Assert().Equal(suite.IntegrationID, integration.ID) + suite.Assert().Equal(suite.IntegrationName, integration.Name) +} + +func (suite *OpenMessagingSuite) TestCanFetchByName() { + match := func(integration gcloudcx.OpenMessagingIntegration) bool { + return integration.Name == suite.IntegrationName + } + integration, err := gcloudcx.FetchBy(context.Background(), suite.Client, match) + suite.Require().NoErrorf(err, "Failed to fetch Open Messaging Integration %s. %s", suite.IntegrationName, err) + suite.Assert().Equal(suite.IntegrationID, integration.ID) + suite.Assert().Equal(suite.IntegrationName, integration.Name) +} + +func (suite *OpenMessagingSuite) TestCanFetchIntegrations() { + integrations, err := gcloudcx.FetchAll[gcloudcx.OpenMessagingIntegration](context.Background(), suite.Client) + suite.Require().NoError(err, "Failed to fetch OpenMessaging Integrations") + if len(integrations) > 0 { + for _, integration := range integrations { + suite.Logger.Record("integration", integration).Infof("Got a integration") + suite.Assert().NotEmpty(integration.ID) + suite.Assert().NotEmpty(integration.Name) + suite.Assert().NotNil(integration.WebhookURL, "WebhookURL should not be nil (%s)", integration.Name) + } + } +} + +func (suite *OpenMessagingSuite) TestCanZZDeleteIntegration() { suite.Require().NotNil(suite.IntegrationID, "IntegrationID should not be nil (TestCanCreateIntegration should run before this test)") - integration := gcloudcx.OpenMessagingIntegration{} - err := suite.Client.Fetch(context.Background(), &integration, suite.IntegrationID) - suite.Require().Nilf(err, "Failed to fetch integration %s, Error: %s", suite.IntegrationID, err) + integration, err := gcloudcx.Fetch[gcloudcx.OpenMessagingIntegration](context.Background(), suite.Client, suite.IntegrationID) + suite.Require().NoErrorf(err, "Failed to fetch integration %s, Error: %s", suite.IntegrationID, err) suite.Logger.Record("integration", integration).Infof("Got a integration") suite.Require().True(integration.IsCreated(), "Integration should be created") err = integration.Delete(context.Background()) - suite.Require().Nilf(err, "Failed to delete integration %s, Error: %s", suite.IntegrationID, err) - err = suite.Client.Fetch(context.Background(), &integration, suite.IntegrationID) - suite.Require().NotNil(err, "Integration should not exist anymore") + suite.Require().NoErrorf(err, "Failed to delete integration %s, Error: %s", suite.IntegrationID, err) + integration, err = gcloudcx.Fetch[gcloudcx.OpenMessagingIntegration](context.Background(), suite.Client, suite.IntegrationID) + suite.Require().Error(err, "Integration should not exist anymore") + suite.Assert().ErrorIsf(err, gcloudcx.NotFoundError, "Expected NotFoundError, got %s", err) + suite.Assert().Truef(errors.Is(err, gcloudcx.NotFoundError), "Expected NotFoundError, got %s", err) + details := gcloudcx.NotFoundError.Clone() + suite.Require().ErrorAsf(err, &details, "Expected NotFoundError but got %s", err) suite.IntegrationID = uuid.Nil } func (suite *OpenMessagingSuite) TestCanUnmarshalIntegration() { integration := gcloudcx.OpenMessagingIntegration{} - err := LoadObject("openmessagingintegration.json", &integration) + err := suite.UnmarshalData("openmessagingintegration.json", &integration) if err != nil { suite.Logger.Errorf("Failed to Unmarshal", err) } - suite.Require().Nilf(err, "Failed to unmarshal OpenMessagingIntegration. %s", err) + suite.Require().NoErrorf(err, "Failed to unmarshal OpenMessagingIntegration. %s", err) suite.Logger.Record("integration", integration).Infof("Got a integration") suite.Assert().Equal("34071108-1569-4cb0-9137-a326b8a9e815", integration.ID.String()) suite.Assert().NotEmpty(integration.CreatedBy.ID) @@ -125,13 +232,11 @@ func (suite *OpenMessagingSuite) TestCanMarshalIntegration() { SelfURI: "/api/v2/users/3e23b1b3-325f-4fbd-8fe0-e88416850c0e", }, CreateStatus: "Initiated", - SelfURI: "/api/v2/conversations/messaging/integrations/open/34071108-1569-4cb0-9137-a326b8a9e815", } data, err := json.Marshal(integration) - suite.Require().Nilf(err, "Failed to marshal OpenMessagingIntegration. %s", err) - expected, err := LoadFile("openmessagingintegration.json") - suite.Require().Nilf(err, "Failed to Load Data. %s", err) + suite.Require().NoErrorf(err, "Failed to marshal OpenMessagingIntegration. %s", err) + expected := suite.LoadTestData("openmessagingintegration.json") suite.Assert().JSONEq(string(expected), string(data)) } @@ -140,13 +245,13 @@ func (suite *OpenMessagingSuite) TestShouldNotUnmarshalIntegrationWithInvalidJSO integration := gcloudcx.OpenMessagingIntegration{} err = json.Unmarshal([]byte(`{"Name": 15}`), &integration) - suite.Assert().NotNil(err, "Data should not have been unmarshaled successfully") + suite.Assert().Error(err, "Data should not have been unmarshaled successfully") } func (suite *OpenMessagingSuite) TestCanUnmarshalOpenMessageChannel() { channel := gcloudcx.OpenMessageChannel{} - err := LoadObject("openmessaging-channel.json", &channel) - suite.Require().Nilf(err, "Failed to unmarshal OpenMessageChannel. %s", err) + err := suite.UnmarshalData("openmessaging-channel.json", &channel) + suite.Require().NoErrorf(err, "Failed to unmarshal OpenMessageChannel. %s", err) suite.Assert().Equal("Open", channel.Platform) suite.Assert().Equal("Private", channel.Type) suite.Assert().Equal("gmAy9zNkhf4ermFvHH9mB5", channel.MessageID) @@ -174,10 +279,9 @@ func (suite *OpenMessagingSuite) TestCanMarshalOpenMessageChannel() { channel.Time = time.Date(2021, 4, 9, 4, 43, 33, 0, time.UTC) data, err := json.Marshal(channel) - suite.Require().Nilf(err, "Failed to marshal OpenMessageChannel. %s", err) + suite.Require().NoErrorf(err, "Failed to marshal OpenMessageChannel. %s", err) suite.Require().NotNil(data, "Marshaled data should not be nil") - expected, err := LoadFile("openmessaging-channel.json") - suite.Require().Nilf(err, "Failed to Load Data. %s", err) + expected := suite.LoadTestData("openmessaging-channel.json") suite.Assert().JSONEq(string(expected), string(data)) } @@ -235,7 +339,7 @@ func (suite *OpenMessagingSuite) TestShouldNotUnmarshalChannelWithInvalidJSON() channel := gcloudcx.OpenMessageChannel{} err = json.Unmarshal([]byte(`{"Platform": 2}`), &channel) - suite.Assert().NotNil(err, "Data should not have been unmarshaled successfully") + suite.Assert().Error(err, "Data should not have been unmarshaled successfully") } func (suite *OpenMessagingSuite) TestShouldNotUnmarshalFromWithInvalidJSON() { @@ -243,12 +347,12 @@ func (suite *OpenMessagingSuite) TestShouldNotUnmarshalFromWithInvalidJSON() { from := gcloudcx.OpenMessageFrom{} err = json.Unmarshal([]byte(`{"idType": 3}`), &from) - suite.Assert().NotNil(err, "Data should not have been unmarshaled successfully") + suite.Assert().Error(err, "Data should not have been unmarshaled successfully") } func (suite *OpenMessagingSuite) TestShouldNotUnmarshalMessageWithInvalidJSON() { _, err := gcloudcx.UnmarshalOpenMessage([]byte(`{"Direction": 6}`)) - suite.Assert().NotNil(err, "Data should not have been unmarshaled successfully") + suite.Assert().Error(err, "Data should not have been unmarshaled successfully") } func (suite *OpenMessagingSuite) TestCanStringifyIntegration() { @@ -261,71 +365,3 @@ func (suite *OpenMessagingSuite) TestCanStringifyIntegration() { integration.Name = "" suite.Assert().Equal(id.String(), integration.String()) } - -// Suite Tools - -func (suite *OpenMessagingSuite) SetupSuite() { - _ = godotenv.Load() - suite.Name = strings.TrimSuffix(reflect.TypeOf(*suite).Name(), "Suite") - suite.Logger = logger.Create("test", - &logger.FileStream{ - Path: fmt.Sprintf("./log/test-%s.log", strings.ToLower(suite.Name)), - Unbuffered: true, - FilterLevel: logger.TRACE, - }, - ).Child("test", "test") - suite.Logger.Infof("Suite Start: %s %s", suite.Name, strings.Repeat("=", 80-14-len(suite.Name))) - - var ( - region = core.GetEnvAsString("PURECLOUD_REGION", "") - clientID = uuid.MustParse(core.GetEnvAsString("PURECLOUD_CLIENTID", "")) - secret = core.GetEnvAsString("PURECLOUD_CLIENTSECRET", "") - token = core.GetEnvAsString("PURECLOUD_CLIENTTOKEN", "") - deploymentID = uuid.MustParse(core.GetEnvAsString("PURECLOUD_DEPLOYMENTID", "")) - ) - suite.Client = gcloudcx.NewClient(&gcloudcx.ClientOptions{ - Region: region, - DeploymentID: deploymentID, - Logger: suite.Logger, - }).SetAuthorizationGrant(&gcloudcx.ClientCredentialsGrant{ - ClientID: clientID, - Secret: secret, - Token: gcloudcx.AccessToken{ - Type: "bearer", - Token: token, - }, - }) - suite.Require().NotNil(suite.Client, "GCloudCX Client is nil") -} - -func (suite *OpenMessagingSuite) TearDownSuite() { - if suite.T().Failed() { - suite.Logger.Warnf("At least one test failed, we are not cleaning") - suite.T().Log("At least one test failed, we are not cleaning") - } else { - suite.Logger.Infof("All tests succeeded, we are cleaning") - } - suite.Logger.Infof("Suite End: %s %s", suite.Name, strings.Repeat("=", 80-12-len(suite.Name))) - suite.Logger.Close() -} - -func (suite *OpenMessagingSuite) BeforeTest(suiteName, testName string) { - suite.Logger.Infof("Test Start: %s %s", testName, strings.Repeat("-", 80-13-len(testName))) - - suite.Start = time.Now() - - // Reuse tokens as much as we can - if !suite.Client.IsAuthorized() { - suite.Logger.Infof("Client is not logged in...") - err := suite.Client.Login(context.Background()) - suite.Require().Nil(err, "Failed to login") - suite.Logger.Infof("Client is now logged in...") - } else { - suite.Logger.Infof("Client is already logged in...") - } -} - -func (suite *OpenMessagingSuite) AfterTest(suiteName, testName string) { - duration := time.Since(suite.Start) - suite.Logger.Record("duration", duration.String()).Infof("Test End: %s %s", testName, strings.Repeat("-", 80-11-len(testName))) -} diff --git a/organization.go b/organization.go index 75da671..e18b81b 100644 --- a/organization.go +++ b/organization.go @@ -3,7 +3,6 @@ package gcloudcx import ( "context" - "github.com/gildas/go-errors" "github.com/gildas/go-logger" "github.com/google/uuid" ) @@ -27,34 +26,6 @@ type Organization struct { logger *logger.Logger `json:"-"` } -// Fetch fetches an Organization -// -// implements Fetchable -func (organization *Organization) Fetch(ctx context.Context, client *Client, parameters ...interface{}) error { - id, name, selfURI, log := client.ParseParameters(ctx, organization, parameters...) - - if id != uuid.Nil { - if err := client.Get(ctx, NewURI("/organizations/%s", id), &organization); err != nil { - return err - } - organization.logger = log - } else if len(selfURI) > 0 { - if err := client.Get(ctx, selfURI, &organization); err != nil { - return err - } - organization.logger = log.Record("id", organization.ID) - } else if len(name) > 0 { - return errors.NotImplemented.WithStack() - } else { - if err := client.Get(ctx, NewURI("/organizations/me"), &organization); err != nil { - return err - } - organization.logger = log.Record("id", organization.ID) - } - organization.client = client - return nil -} - // GetMyOrganization retrives the current Organization func (client *Client) GetMyOrganization(context context.Context) (*Organization, error) { organization := &Organization{} @@ -66,20 +37,45 @@ func (client *Client) GetMyOrganization(context context.Context) (*Organization, return organization, nil } +// Initialize initializes the object +// +// accepted parameters: *gcloufcx.Client, *logger.Logger +// +// implements Initializable +func (organization *Organization) Initialize(parameters ...interface{}) { + for _, raw := range parameters { + switch parameter := raw.(type) { + case *Client: + organization.client = parameter + case *logger.Logger: + organization.logger = parameter.Child("organization", "organization", "id", organization.ID) + } + } +} + // GetID gets the identifier of this -// implements Identifiable +// +// implements Identifiable func (organization Organization) GetID() uuid.UUID { return organization.ID } // GetURI gets the URI of this -// implements Addressable -func (organization Organization) GetURI() URI { - return organization.SelfURI +// +// implements Addressable +func (organization Organization) GetURI(ids ...uuid.UUID) URI { + if len(ids) > 0 { + return NewURI("/api/v2/organizations/%s", ids[0]) + } + if organization.ID != uuid.Nil { + return NewURI("/api/v2/organizations/%s", organization.ID) + } + return URI("/api/v2/organizations/") } // String gets a string version -// implements the fmt.Stringer interface +// +// implements the fmt.Stringer interface func (organization Organization) String() string { if len(organization.Name) > 0 { return organization.Name diff --git a/organization_test.go b/organization_test.go index affb927..d0c51d5 100644 --- a/organization_test.go +++ b/organization_test.go @@ -35,42 +35,14 @@ func TestOrganizationSuite(t *testing.T) { suite.Run(t, new(OrganizationSuite)) } -func (suite *OrganizationSuite) TestCanFetchMyOrganization() { - organization := gcloudcx.Organization{} - err := suite.Client.Fetch(context.Background(), &organization) - if err != nil { - suite.Logger.Errorf("Failed", err) - } - suite.Require().Nil(err, "Failed to fetch my Organization") - suite.Assert().Equal(suite.OrganizationID, organization.GetID(), "Client's Organization ID is not the same") - suite.Assert().Equal(suite.OrganizationName, organization.String(), "Client's Organization Name is not the same") - suite.Assert().NotEmpty(organization.Features, "Client's Organization has no features") - suite.T().Logf("Organization: %s", organization.Name) - suite.Logger.Record("org", organization).Infof("Organization Details") -} - -func (suite *OrganizationSuite) TestCanFetchOrganizationByID() { - organization := gcloudcx.Organization{} - err := suite.Client.Fetch(context.Background(), &organization, suite.OrganizationID) - if err != nil { - suite.Logger.Errorf("Failed", err) - } - suite.Require().Nil(err, "Failed to fetch my Organization") - suite.Assert().Equal(suite.OrganizationID, organization.GetID(), "Client's Organization ID is not the same") - suite.Assert().Equal(suite.OrganizationName, organization.String(), "Client's Organization Name is not the same") - suite.Assert().NotEmpty(organization.Features, "Client's Organization has no features") - suite.T().Logf("Organization: %s", organization.Name) - suite.Logger.Record("org", organization).Infof("Organization Details") -} - -// Suite Tools - +// ***************************************************************************** +// #region: Suite Tools {{{ func (suite *OrganizationSuite) SetupSuite() { var err error var value string _ = godotenv.Load() - suite.Name = strings.TrimSuffix(reflect.TypeOf(*suite).Name(), "Suite") + suite.Name = strings.TrimSuffix(reflect.TypeOf(suite).Elem().Name(), "Suite") suite.Logger = logger.Create("test", &logger.FileStream{ Path: fmt.Sprintf("./log/test-%s.log", strings.ToLower(suite.Name)), @@ -86,7 +58,7 @@ func (suite *OrganizationSuite) SetupSuite() { suite.Require().NotEmpty(value, "PURECLOUD_CLIENTID is not set") clientID, err := uuid.Parse(value) - suite.Require().Nil(err, "PURECLOUD_CLIENTID is not a valid UUID") + suite.Require().NoError(err, "PURECLOUD_CLIENTID is not a valid UUID") secret := core.GetEnvAsString("PURECLOUD_CLIENTSECRET", "") suite.Require().NotEmpty(secret, "PURECLOUD_CLIENTSECRET is not set") @@ -95,13 +67,13 @@ func (suite *OrganizationSuite) SetupSuite() { suite.Require().NotEmpty(value, "PURECLOUD_DEPLOYMENTID is not set") deploymentID, err := uuid.Parse(value) - suite.Require().Nil(err, "PURECLOUD_DEPLOYMENTID is not a valid UUID") + suite.Require().NoError(err, "PURECLOUD_DEPLOYMENTID is not a valid UUID") value = core.GetEnvAsString("ORGANIZATION_ID", "") suite.Require().NotEmpty(value, "ORGANIZATION_ID is not set in your environment") suite.OrganizationID, err = uuid.Parse(value) - suite.Require().Nil(err, "ORGANIZATION_ID is not a valid UUID") + suite.Require().NoError(err, "ORGANIZATION_ID is not a valid UUID") suite.OrganizationName = core.GetEnvAsString("ORGANIZATION_NAME", "") suite.Require().NotEmpty(suite.OrganizationName, "ORGANIZATION_NAME is not set in your environment") @@ -136,7 +108,7 @@ func (suite *OrganizationSuite) BeforeTest(suiteName, testName string) { if !suite.Client.IsAuthorized() { suite.Logger.Infof("Client is not logged in...") err := suite.Client.Login(context.Background()) - suite.Require().Nil(err, "Failed to login") + suite.Require().NoError(err, "Failed to login") suite.Logger.Infof("Client is now logged in...") } else { suite.Logger.Infof("Client is already logged in...") @@ -147,3 +119,32 @@ func (suite *OrganizationSuite) AfterTest(suiteName, testName string) { duration := time.Since(suite.Start) suite.Logger.Record("duration", duration.String()).Infof("Test End: %s %s", testName, strings.Repeat("-", 80-11-len(testName))) } + +// #endregion: Suite Tools }}} +// ***************************************************************************** + +func (suite *OrganizationSuite) TestCanFetchMyOrganization() { + organization, err := suite.Client.GetMyOrganization(context.Background()) + if err != nil { + suite.Logger.Errorf("Failed", err) + } + suite.Require().NoError(err, "Failed to fetch my Organization") + suite.Assert().Equal(suite.OrganizationID, organization.GetID(), "Client's Organization ID is not the same") + suite.Assert().Equal(suite.OrganizationName, organization.String(), "Client's Organization Name is not the same") + suite.Assert().NotEmpty(organization.Features, "Client's Organization has no features") + suite.T().Logf("Organization: %s", organization.Name) + suite.Logger.Record("org", organization).Infof("Organization Details") +} + +func (suite *OrganizationSuite) TestCanFetchOrganizationByID() { + organization, err := gcloudcx.Fetch[gcloudcx.Organization](context.Background(), suite.Client, suite.OrganizationID) + if err != nil { + suite.Logger.Errorf("Failed", err) + } + suite.Require().NoError(err, "Failed to fetch my Organization") + suite.Assert().Equal(suite.OrganizationID, organization.GetID(), "Client's Organization ID is not the same") + suite.Assert().Equal(suite.OrganizationName, organization.String(), "Client's Organization Name is not the same") + suite.Assert().NotEmpty(organization.Features, "Client's Organization has no features") + suite.T().Logf("Organization: %s", organization.Name) + suite.Logger.Record("org", organization).Infof("Organization Details") +} diff --git a/participant.go b/participant.go index 52ead97..6b94952 100644 --- a/participant.go +++ b/participant.go @@ -184,7 +184,7 @@ func (participant Participant) MarshalJSON() ([]byte, error) { userURI := URI("") if participant.User != nil { userID = participant.User.ID - userURI = participant.User.SelfURI + userURI = participant.User.GetURI() } type surrogate Participant data, err := json.Marshal(struct { @@ -221,7 +221,7 @@ func (participant *Participant) UnmarshalJSON(payload []byte) (err error) { } *participant = Participant(inner.surrogate) if participant.User == nil && len(inner.UserID) > 0 { - participant.User = &User{ID: inner.UserID, SelfURI: inner.UserURI} + participant.User = &User{ID: inner.UserID} } participant.AlertingTimeout = time.Duration(inner.AlertingTimeoutMs) * time.Millisecond participant.WrapupTimeout = time.Duration(inner.WrapupTimeoutMs) * time.Millisecond diff --git a/queue.go b/queue.go index 348bf6b..09cf38f 100644 --- a/queue.go +++ b/queue.go @@ -1,9 +1,7 @@ package gcloudcx import ( - "context" "encoding/json" - "net/url" "time" "github.com/gildas/go-errors" @@ -36,60 +34,20 @@ type RoutingTarget struct { Address string `json:"targetAddress,omitempty"` } -// Fetch fetches a queue +// Initialize initializes the object // -// implements Fetchable -func (queue *Queue) Fetch(ctx context.Context, client *Client, parameters ...interface{}) error { - id, name, selfURI, log := client.ParseParameters(ctx, queue, parameters...) - - if id != uuid.Nil { - if err := client.Get(ctx, NewURI("/groups/%s", id), &queue); err != nil { - return err - } - queue.logger = log - } else if len(selfURI) > 0 { - if err := client.Get(ctx, selfURI, &queue); err != nil { - return err - } - queue.logger = log.Record("id", queue.ID) - } else if len(name) > 0 { - return errors.NotImplemented.WithStack() - } - queue.client = client - return nil -} - -// FindQueueByName finds a Queue by its name -func (client *Client) FindQueueByName(context context.Context, name string) (*Queue, error) { - response := struct { - Entities []*Queue `json:"entities"` - PageSize int64 `json:"pageSize"` - PageNumber int64 `json:"pageNumber"` - PageCount int64 `json:"pageCount"` - PageTotal int64 `json:"pageTotal"` - SelfURI string `json:"selfUri"` - FirstURI string `json:"firstUrl"` - LastURI string `json:"lastUri"` - }{} - query := url.Values{} - query.Add("name", name) - err := client.Get(context, NewURI("/routing/queues?%s", query.Encode()), &response) - if err != nil { - return nil, errors.WithStack(err) - } - for _, queue := range response.Entities { - if queue.Name == name { - queue.client = client - queue.logger = client.Logger.Child("queue", "queue", "id", queue.ID) - if queue.CreatedBy != nil { - queue.CreatedBy.client = client - queue.CreatedBy.logger = client.Logger.Child("user", "user", "id", queue.CreatedBy.ID) - } - return queue, nil +// accepted parameters: *gcloufcx.Client, *logger.Logger +// +// implements Initializable +func (queue *Queue) Initialize(parameters ...interface{}) { + for _, raw := range parameters { + switch parameter := raw.(type) { + case *Client: + queue.client = parameter + case *logger.Logger: + queue.logger = parameter.Child("queue", "queue", "id", queue.ID) } } - // TODO: read all pages!!! - return nil, errors.NotFound.With("queue", name) } // GetID gets the identifier of this @@ -99,9 +57,16 @@ func (queue Queue) GetID() uuid.UUID { } // GetURI gets the URI of this -// implements Addressable -func (queue Queue) GetURI() URI { - return queue.SelfURI +// +// implements Addressable +func (queue Queue) GetURI(ids ...uuid.UUID) URI { + if len(ids) > 0 { + return NewURI("/api/v2/routing/queues/%s", ids[0]) + } + if queue.ID != uuid.Nil { + return NewURI("/api/v2/routing/queues/%s", queue.ID) + } + return URI("/api/v2/routing/queues/") } func (queue Queue) String() string { diff --git a/queue_test.go b/queue_test.go new file mode 100644 index 0000000..b7c650c --- /dev/null +++ b/queue_test.go @@ -0,0 +1,202 @@ +package gcloudcx_test + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + "time" + + "github.com/gildas/go-core" + "github.com/gildas/go-logger" + "github.com/google/uuid" + "github.com/joho/godotenv" + "github.com/stretchr/testify/suite" + + "github.com/gildas/go-gcloudcx" +) + +type QueueSuite struct { + suite.Suite + Name string + Logger *logger.Logger + Start time.Time + + QueueID uuid.UUID + QueueName string + Client *gcloudcx.Client +} + +func TestQueueSuite(t *testing.T) { + suite.Run(t, new(QueueSuite)) +} + +// ***************************************************************************** +// #region: Suite Tools {{{ +func (suite *QueueSuite) SetupSuite() { + var err error + var value string + + _ = godotenv.Load() + suite.Name = strings.TrimSuffix(reflect.TypeOf(suite).Elem().Name(), "Suite") + suite.Logger = logger.Create("test", + &logger.FileStream{ + Path: fmt.Sprintf("./log/test-%s.log", strings.ToLower(suite.Name)), + Unbuffered: true, + FilterLevel: logger.TRACE, + }, + ).Child("test", "test") + suite.Logger.Infof("Suite Start: %s %s", suite.Name, strings.Repeat("=", 80-14-len(suite.Name))) + + var ( + region = core.GetEnvAsString("PURECLOUD_REGION", "") + clientID = uuid.MustParse(core.GetEnvAsString("PURECLOUD_CLIENTID", "")) + secret = core.GetEnvAsString("PURECLOUD_CLIENTSECRET", "") + ) + + value = core.GetEnvAsString("QUEUE_ID", "") + suite.Require().NotEmpty(value, "QUEUE_ID is not set in your environment") + + suite.QueueID, err = uuid.Parse(value) + suite.Require().NoError(err, "USER_ID is not a valid UUID") + + suite.QueueName = core.GetEnvAsString("QUEUE_NAME", "") + suite.Require().NotEmpty(suite.QueueName, "QUEUE_NAME is not set in your environment") + + suite.Client = gcloudcx.NewClient(&gcloudcx.ClientOptions{ + Region: region, + Logger: suite.Logger, + }).SetAuthorizationGrant(&gcloudcx.ClientCredentialsGrant{ + ClientID: clientID, + Secret: secret, + }) + suite.Require().NotNil(suite.Client, "GCloudCX Client is nil") +} + +func (suite *QueueSuite) TearDownSuite() { + if suite.T().Failed() { + suite.Logger.Warnf("At least one test failed, we are not cleaning") + suite.T().Log("At least one test failed, we are not cleaning") + } else { + suite.Logger.Infof("All tests succeeded, we are cleaning") + } + suite.Logger.Infof("Suite End: %s %s", suite.Name, strings.Repeat("=", 80-12-len(suite.Name))) + suite.Logger.Close() +} + +func (suite *QueueSuite) BeforeTest(suiteName, testName string) { + suite.Logger.Infof("Test Start: %s %s", testName, strings.Repeat("-", 80-13-len(testName))) + suite.Start = time.Now() + + // Reuse tokens as much as we can + if !suite.Client.IsAuthorized() { + suite.Logger.Infof("Client is not logged in...") + err := suite.Client.Login(context.Background()) + suite.Require().NoError(err, "Failed to login") + suite.Logger.Infof("Client is now logged in...") + } else { + suite.Logger.Infof("Client is already logged in...") + } +} + +func (suite *QueueSuite) AfterTest(suiteName, testName string) { + duration := time.Since(suite.Start) + suite.Logger.Record("duration", duration.String()).Infof("Test End: %s %s", testName, strings.Repeat("-", 80-11-len(testName))) +} + +func (suite *QueueSuite) LoadTestData(filename string) []byte { + data, err := os.ReadFile(filepath.Join(".", "testdata", filename)) + suite.Require().NoErrorf(err, "Failed to Load Data. %s", err) + return data +} + +func (suite *QueueSuite) UnmarshalData(filename string, v interface{}) error { + data := suite.LoadTestData(filename) + suite.Logger.Infof("Loaded %s: %s", filename, string(data)) + return json.Unmarshal(data, v) +} + +// #endregion: Suite Tools }}} +// ***************************************************************************** + +func (suite *QueueSuite) TestCanUnmarshal() { + user := gcloudcx.User{} + err := suite.UnmarshalData("user.json", &user) + suite.Require().NoErrorf(err, "Failed to unmarshal user. %s", err) + suite.Logger.Record("User", user).Infof("Got a user") + suite.Assert().NotEmpty(user.ID) + suite.Assert().Equal("John Doe", user.Name) +} + +func (suite *QueueSuite) TestCanMarshal() { + user := gcloudcx.User{ + ID: uuid.MustParse("06ffcd2e-1ada-412e-a5f5-30d7853246dd"), + Name: "John Doe", + UserName: "john.doe@acme.com", + Mail: "john.doe@acme.com", + Title: "Junior", + Division: &gcloudcx.Division{ + ID: uuid.MustParse("06ffcd2e-1ada-412e-a5f5-30d7853246dd"), + Name: "", + SelfURI: "/api/v2/authorization/divisions/06ffcd2e-1ada-412e-a5f5-30d7853246dd", + }, + Chat: &gcloudcx.Jabber{ + ID: "98765432d220541234567654@genesysapacanz.orgspan.com", + }, + Addresses: []*gcloudcx.Contact{}, + PrimaryContact: []*gcloudcx.Contact{ + { + Type: "PRIMARY", + MediaType: "EMAIL", + Address: "john.doe@acme.com", + }, + }, + Images: []*gcloudcx.UserImage{ + { + Resolution: "x96", + ImageURL: core.Must(url.Parse("https://prod-apse2-inin-directory-service-profile.s3-ap-southeast-2.amazonaws.com/7fac0a12/4643/4d0e/86f3/2467894311b5.jpg")).(*url.URL), + }, + }, + AcdAutoAnswer: false, + State: "active", + Version: 29, + } + + data, err := json.Marshal(user) + suite.Require().NoErrorf(err, "Failed to marshal User. %s", err) + expected := suite.LoadTestData("user.json") + suite.Assert().JSONEq(string(expected), string(data)) +} + +func (suite *QueueSuite) TestCanFetchByID() { + queue, err := gcloudcx.Fetch[gcloudcx.Queue](context.Background(), suite.Client, suite.QueueID) + suite.Require().NoErrorf(err, "Failed to fetch Queue %s. %s", suite.QueueID, err) + suite.Assert().Equal(suite.QueueID, queue.ID) + suite.Assert().Equal(suite.QueueName, queue.Name) +} + +func (suite *QueueSuite) TestCanFetchByNameSlow() { + match := func(queue gcloudcx.Queue) bool { + return queue.Name == suite.QueueName + } + queue, err := gcloudcx.FetchBy(context.Background(), suite.Client, match) + suite.Require().NoErrorf(err, "Failed to fetch Queue %s. %s", suite.QueueID, err) + suite.Assert().Equal(suite.QueueID, queue.ID) + suite.Assert().Equal(suite.QueueName, queue.Name) +} + +func (suite *QueueSuite) TestCanFetchByName() { + // Calling with a Query will speed up the search significantly + match := func(recipient gcloudcx.Queue) bool { + return true + } + queue, err := gcloudcx.FetchBy(context.Background(), suite.Client, match, gcloudcx.Query{"name": suite.QueueName}) + suite.Require().NoErrorf(err, "Failed to fetch Queue %s. %s", suite.QueueName, err) + suite.Assert().Equal(suite.QueueID, queue.ID) + suite.Assert().Equal(suite.QueueName, queue.Name) +} diff --git a/request.go b/request.go index 1cef88a..4dc9381 100644 --- a/request.go +++ b/request.go @@ -80,8 +80,10 @@ func (client *Client) SendRequest(context context.Context, path URI, options *re res, err := request.Send(options, results) duration := time.Since(start) log = log.Record("duration", duration) + correlationID := "" if res != nil { - log = log.Record("correlationId", res.Headers.Get("Inin-Correlation-Id")) + correlationID = res.Headers.Get("Inin-Correlation-Id") + log = log.Record("correlationId", correlationID) } if err != nil { urlError := &url.Error{} @@ -106,12 +108,13 @@ func (client *Client) SendRequest(context context.Context, path URI, options *re if jsonerr := res.UnmarshalContentJSON(&apiError); jsonerr != nil { return errors.Wrap(err, "Failed to extract an error from the response") } + apiError.CorrelationID = correlationID + return apiError.WithStack() } + // Sometimes we do not get a response with a Gcloud error, but a generic error apiError.Status = details.Code apiError.Code = details.ID - if res != nil { - apiError.CorrelationID = res.Headers.Get("Inin-Correlation-Id") - } + apiError.CorrelationID = correlationID if strings.HasPrefix(apiError.Message, "authentication failed") { apiError.Status = errors.HTTPUnauthorized.Code apiError.Code = errors.HTTPUnauthorized.ID diff --git a/responsemanagement_library.go b/responsemanagement_library.go index eef5208..d5676d4 100644 --- a/responsemanagement_library.go +++ b/responsemanagement_library.go @@ -1,11 +1,8 @@ package gcloudcx import ( - "context" - "strings" "time" - "github.com/gildas/go-errors" "github.com/google/uuid" ) @@ -21,59 +18,42 @@ type ResponseManagementLibrary struct { SelfURI URI `json:"selfUri,omitempty"` } -// Fetch fetches this from the given Client +// Initialize initializes the object // -// implements Fetchable -func (library *ResponseManagementLibrary) Fetch(ctx context.Context, client *Client, parameters ...interface{}) error { - id, name, uri, log := client.ParseParameters(ctx, library, parameters...) - if len(uri) > 0 { - return client.Get(ctx, uri, library) - } - if id != uuid.Nil { - return client.Get(ctx, NewURI("/responsemanagement/libraries/%s", id), library) - } - if len(name) > 0 { - nameLowercase := strings.ToLower(name) - entities := struct { - Libraries []ResponseManagementLibrary `json:"entities"` - paginatedEntities - }{} - for pageNumber := 1; ; pageNumber++ { - log.Debugf("Fetching page %d", pageNumber) - if err := client.Get(ctx, NewURI("/responsemanagement/libraries/?pageNumber=%d", pageNumber), &entities); err != nil { - return err - } - for _, entity := range entities.Libraries { - if strings.Compare(strings.ToLower(entity.Name), nameLowercase) == 0 { - *library = entity - return nil - } - } - if pageNumber >= entities.PageCount { - break - } - } - } - return errors.NotFound.With("ResponseManagementLibrary") +// implements Initializable +func (library *ResponseManagementLibrary) Initialize(parameters ...interface{}) { } // GetID gets the identifier of this // -// implements Identifiable -func (library *ResponseManagementLibrary) GetID() uuid.UUID { +// implements Identifiable +func (library ResponseManagementLibrary) GetID() uuid.UUID { return library.ID } +// GetURI gets the URI of this +// +// implements Addressable +func (library ResponseManagementLibrary) GetURI(ids ...uuid.UUID) URI { + if len(ids) > 0 { + return NewURI("/api/v2/responsemanagement/libraries/%s", ids[0]) + } + if library.ID != uuid.Nil { + return NewURI("/api/v2/responsemanagement/libraries/%s", library.ID) + } + return URI("/api/v2/responsemanagement/libraries/") +} + // GetType gets the identifier of this // -// implements Identifiable +// implements Identifiable func (library *ResponseManagementLibrary) GetType() string { return "responsemanagement.library" } // String gets a string version // -// implements the fmt.Stringer interface +// implements the fmt.Stringer interface func (library ResponseManagementLibrary) String() string { if len(library.Name) > 0 { return library.Name diff --git a/responsemanagement_response.go b/responsemanagement_response.go index fa8386c..e9eb934 100644 --- a/responsemanagement_response.go +++ b/responsemanagement_response.go @@ -2,7 +2,6 @@ package gcloudcx import ( "context" - "strings" "time" "github.com/gildas/go-errors" @@ -53,63 +52,70 @@ type ResponseManagementSubstitution struct { Default string `json:"defaultValue"` } -// Fetch fetches this from the given Client +// Initialize initializes the object // -// implements Fetchable -func (response *ResponseManagementResponse) Fetch(ctx context.Context, client *Client, parameters ...interface{}) error { - // TODO: Allow to filter per Library ID - id, name, uri, _ := client.ParseParameters(ctx, response, parameters...) - if len(uri) > 0 { - return client.Get(ctx, uri, response) - } - if id != uuid.Nil { - return client.Get(ctx, NewURI("/responsemanagement/responses/%s", id), response) - } - if len(name) > 0 { - nameLowercase := strings.ToLower(name) - results := struct { - Results struct { - Responses []ResponseManagementResponse `json:"entities"` - paginatedEntities - } `json:"results"` - }{} - err := client.Post( - ctx, - NewURI("/responsemanagement/responses/query"), - ResponseManagementQuery{ - Filters: []ResponseManagementQueryFilter{ - {Name: "name", Operator: "EQUALS", Values: []string{name}}, - }, - }, - &results, - ) - if err != nil { - return err - } - for _, entity := range results.Results.Responses { - if strings.Compare(strings.ToLower(entity.Name), nameLowercase) == 0 { - *response = entity - return nil - } - } - return errors.NotFound.With("ResponseManagementResponse", name) - } - return errors.NotFound.With("ResponseManagementResponse") +// implements Initializable +func (response *ResponseManagementResponse) Initialize(parameters ...interface{}) { } // GetID gets the identifier of this // -// implements Identifiable -func (response *ResponseManagementResponse) GetID() uuid.UUID { +// implements Identifiable +func (response ResponseManagementResponse) GetID() uuid.UUID { return response.ID } +// GetURI gets the URI of this +// +// implements Addressable +func (response ResponseManagementResponse) GetURI(ids ...uuid.UUID) URI { + if len(ids) > 0 { + return NewURI("/api/v2/responsemanagement/responses/%s", ids[0]) + } + if response.ID != uuid.Nil { + return NewURI("/api/v2/responsemanagement/responses/%s", response.ID) + } + return URI("/api/v2/responsemanagement/responses/") +} + // String gets a string version // -// implements the fmt.Stringer interface +// implements the fmt.Stringer interface func (response ResponseManagementResponse) String() string { if len(response.Name) > 0 { return response.Name } return response.ID.String() } + +func (response ResponseManagementResponse) FetchByFilters(context context.Context, client *Client, filters ...ResponseManagementQueryFilter) (*ResponseManagementResponse, error) { + results := struct { + Results struct { + Responses []ResponseManagementResponse `json:"entities"` + paginatedEntities + } `json:"results"` + }{} + err := client.Post( + context, + NewURI("/responsemanagement/responses/query"), + ResponseManagementQuery{ + Filters: filters, + }, + &results, + ) + if err != nil { + return nil, err + } + if len(results.Results.Responses) == 0 { + return nil, errors.NotFound.With("ResponseManagementResponse") + } + /* + for _, entity := range results.Results.Responses { + if strings.Compare(strings.ToLower(entity.Name), nameLowercase) == 0 { + *response = entity + return nil + } + } + */ + return &results.Results.Responses[0], nil +} diff --git a/responsemanagement_test.go b/responsemanagement_test.go index 34ef17c..16b3824 100644 --- a/responsemanagement_test.go +++ b/responsemanagement_test.go @@ -35,78 +35,14 @@ func TestResponseManagementSuite(t *testing.T) { suite.Run(t, new(ResponseManagementSuite)) } -func (suite *ResponseManagementSuite) TestCanFetchLibraryByID() { - library := gcloudcx.ResponseManagementLibrary{} - err := suite.Client.Fetch(context.Background(), &library, suite.LibraryID) - if err != nil { - suite.Logger.Errorf("Failed", err) - } - suite.Require().Nilf(err, "Failed to fetch Response Management Library, Error: %s", err) - suite.Assert().Equal(suite.LibraryID, library.GetID(), "Library ID is not the same") - suite.Assert().Equal(suite.LibraryName, library.String(), "Library Name is not the same") - suite.Logger.Record("library", library).Infof("Library Details") -} - -func (suite *ResponseManagementSuite) TestCanFetchLibraryByName() { - library := gcloudcx.ResponseManagementLibrary{} - err := suite.Client.Fetch(context.Background(), &library, suite.LibraryName) - if err != nil { - suite.Logger.Errorf("Failed", err) - } - suite.Require().Nil(err, "Failed to fetch Response Management Library, Error: %s", err) - suite.Assert().Equal(suite.LibraryID, library.GetID(), "Library ID is not the same") - suite.Assert().Equal(suite.LibraryName, library.String(), "Library Name is not the same") - suite.Logger.Record("library", library).Infof("Library Details") -} - -func (suite *ResponseManagementSuite) TestCanFetchResponseByID() { - response := gcloudcx.ResponseManagementResponse{} - err := suite.Client.Fetch(context.Background(), &response, suite.ResponseID) - if err != nil { - suite.Logger.Errorf("Failed", err) - } - suite.Require().Nil(err, "Failed to fetch Response Management Library, Error: %s", err) - suite.Assert().Equal(suite.ResponseID, response.GetID(), "Client's Organization ID is not the same") - suite.Assert().Equal(suite.ResponseName, response.String(), "Client's Organization Name is not the same") - suite.Logger.Record("response", response).Infof("Response Details") -} - -func (suite *ResponseManagementSuite) TestCanFetchResponseByName() { - response := gcloudcx.ResponseManagementResponse{} - err := suite.Client.Fetch(context.Background(), &response, suite.ResponseName) - if err != nil { - suite.Logger.Errorf("Failed", err) - } - suite.Require().Nil(err, "Failed to fetch Response Management Response, Error: %s", err) - suite.Assert().Equal(suite.ResponseID, response.GetID(), "Response ID is not the same") - suite.Assert().Equal(suite.ResponseName, response.String(), "Response Name is not the same") - suite.Logger.Record("response", response).Infof("Response Details") -} - -func (suite *ResponseManagementSuite) TestShouldFailFetchingLibraryWithUnknownName() { - library := gcloudcx.ResponseManagementLibrary{} - err := suite.Client.Fetch(context.Background(), &library, "unknown library") - suite.Require().NotNil(err, "Should have failed to fetch Response Management Library") - suite.Logger.Errorf("Expected Failure", err) - suite.Assert().ErrorIs(err, errors.NotFound, "Should have failed to fetch Response Management Library") -} - -func (suite *ResponseManagementSuite) TestShouldFailFetchingResponseWithUnknownName() { - response := gcloudcx.ResponseManagementLibrary{} - err := suite.Client.Fetch(context.Background(), &response, "unknown response") - suite.Require().NotNil(err, "Should have failed to fetch Response Management Response") - suite.Logger.Errorf("Expected Failure", err) - suite.Assert().ErrorIs(err, errors.NotFound, "Should have failed to fetch Response Management Response") -} - -// Suite Tools - +// ***************************************************************************** +// #region: Suite Tools {{{ func (suite *ResponseManagementSuite) SetupSuite() { var err error var value string _ = godotenv.Load() - suite.Name = strings.TrimSuffix(reflect.TypeOf(*suite).Name(), "Suite") + suite.Name = strings.TrimSuffix(reflect.TypeOf(suite).Elem().Name(), "Suite") suite.Logger = logger.Create("test", &logger.FileStream{ Path: fmt.Sprintf("./log/test-%s.log", strings.ToLower(suite.Name)), @@ -122,7 +58,7 @@ func (suite *ResponseManagementSuite) SetupSuite() { suite.Require().NotEmpty(value, "PURECLOUD_CLIENTID is not set") clientID, err := uuid.Parse(value) - suite.Require().Nil(err, "PURECLOUD_CLIENTID is not a valid UUID") + suite.Require().NoError(err, "PURECLOUD_CLIENTID is not a valid UUID") secret := core.GetEnvAsString("PURECLOUD_CLIENTSECRET", "") suite.Require().NotEmpty(secret, "PURECLOUD_CLIENTSECRET is not set") @@ -131,13 +67,13 @@ func (suite *ResponseManagementSuite) SetupSuite() { suite.Require().NotEmpty(value, "PURECLOUD_DEPLOYMENTID is not set") deploymentID, err := uuid.Parse(value) - suite.Require().Nil(err, "PURECLOUD_DEPLOYMENTID is not a valid UUID") + suite.Require().NoError(err, "PURECLOUD_DEPLOYMENTID is not a valid UUID") value = core.GetEnvAsString("RESPONSE_MANAGEMENT_LIBRARY_ID", "") suite.Require().NotEmpty(value, "RESPONSE_MANAGEMENT_LIBRARY_ID is not set in your environment") suite.LibraryID, err = uuid.Parse(value) - suite.Require().Nil(err, "RESPONSE_MANAGEMENT_LIBRARY_ID is not a valid UUID") + suite.Require().NoError(err, "RESPONSE_MANAGEMENT_LIBRARY_ID is not a valid UUID") suite.LibraryName = core.GetEnvAsString("RESPONSE_MANAGEMENT_LIBRARY_NAME", "") suite.Require().NotEmpty(suite.LibraryName, "RESPONSE_MANAGEMENT_LIBRARY_NAME is not set in your environment") @@ -146,7 +82,7 @@ func (suite *ResponseManagementSuite) SetupSuite() { suite.Require().NotEmpty(value, "RESPONSE_MANAGEMENT_RESPONSE_ID is not set in your environment") suite.ResponseID, err = uuid.Parse(value) - suite.Require().Nil(err, "RESPONSE_MANAGEMENT_RESPONSE_ID is not a valid UUID") + suite.Require().NoError(err, "RESPONSE_MANAGEMENT_RESPONSE_ID is not a valid UUID") suite.ResponseName = core.GetEnvAsString("RESPONSE_MANAGEMENT_RESPONSE_NAME", "") suite.Require().NotEmpty(suite.ResponseName, "RESPONSE_MANAGEMENT_RESPONSE_NAME is not set in your environment") @@ -181,7 +117,7 @@ func (suite *ResponseManagementSuite) BeforeTest(suiteName, testName string) { if !suite.Client.IsAuthorized() { suite.Logger.Infof("Client is not logged in...") err := suite.Client.Login(context.Background()) - suite.Require().Nil(err, "Failed to login") + suite.Require().NoError(err, "Failed to login") suite.Logger.Infof("Client is now logged in...") } else { suite.Logger.Infof("Client is already logged in...") @@ -192,3 +128,89 @@ func (suite *ResponseManagementSuite) AfterTest(suiteName, testName string) { duration := time.Since(suite.Start) suite.Logger.Record("duration", duration.String()).Infof("Test End: %s %s", testName, strings.Repeat("-", 80-11-len(testName))) } + +// #endregion: Suite Tools }}} +// ***************************************************************************** + +func (suite *ResponseManagementSuite) TestCanFetchLibraryByID() { + library, err := gcloudcx.Fetch[gcloudcx.ResponseManagementLibrary](context.Background(), suite.Client, suite.LibraryID) + if err != nil { + suite.Logger.Errorf("Failed", err) + } + suite.Require().NoErrorf(err, "Failed to fetch Response Management Library, Error: %s", err) + suite.Assert().Equal(suite.LibraryID, library.GetID(), "Library ID is not the same") + suite.Assert().Equal(suite.LibraryName, library.String(), "Library Name is not the same") + suite.Logger.Record("library", library).Infof("Library Details") +} + +func (suite *ResponseManagementSuite) TestCanFetchLibraryByName() { + match := func(library gcloudcx.ResponseManagementLibrary) bool { + return library.Name == suite.LibraryName + } + library, err := gcloudcx.FetchBy(context.Background(), suite.Client, match) + if err != nil { + suite.Logger.Errorf("Failed", err) + } + suite.Require().NoErrorf(err, "Failed to fetch Response Management Library, Error: %s", err) + suite.Assert().Equal(suite.LibraryID, library.GetID(), "Library ID is not the same") + suite.Assert().Equal(suite.LibraryName, library.String(), "Library Name is not the same") + suite.Logger.Record("library", library).Infof("Library Details") +} + +func (suite *ResponseManagementSuite) TestCanFetchResponseByID() { + response, err := gcloudcx.Fetch[gcloudcx.ResponseManagementResponse](context.Background(), suite.Client, suite.ResponseID) + if err != nil { + suite.Logger.Errorf("Failed", err) + } + suite.Require().NoErrorf(err, "Failed to fetch Response Management Library, Error: %s", err) + suite.Assert().Equal(suite.ResponseID, response.GetID(), "Client's Organization ID is not the same") + suite.Assert().Equal(suite.ResponseName, response.String(), "Client's Organization Name is not the same") + suite.Logger.Record("response", response).Infof("Response Details") +} + +func (suite *ResponseManagementSuite) TestCanFetchResponseByFilters() { + response, err := gcloudcx.ResponseManagementResponse{}.FetchByFilters(context.Background(), suite.Client, gcloudcx.ResponseManagementQueryFilter{ + Name: "name", Operator: "EQUALS", Values: []string{suite.ResponseName}, + }) + if err != nil { + suite.Logger.Errorf("Failed", err) + } + suite.Require().NoErrorf(err, "Failed to fetch Response Management Response, Error: %s", err) + suite.Assert().Equal(suite.ResponseID, response.GetID(), "Response ID is not the same") + suite.Assert().Equal(suite.ResponseName, response.String(), "Response Name is not the same") + suite.Logger.Record("response", response).Infof("Response Details") +} + +func (suite *ResponseManagementSuite) TestCanFetchResponseByName() { + match := func(response gcloudcx.ResponseManagementResponse) bool { + return response.Name == suite.ResponseName + } + response, err := gcloudcx.FetchBy(context.Background(), suite.Client, match, gcloudcx.Query{"libraryId": suite.LibraryID}) + if err != nil { + suite.Logger.Errorf("Failed", err) + } + suite.Require().NoErrorf(err, "Failed to fetch Response Management Response, Error: %s", err) + suite.Assert().Equal(suite.ResponseID, response.GetID(), "Response ID is not the same") + suite.Assert().Equal(suite.ResponseName, response.String(), "Response Name is not the same") + suite.Logger.Record("response", response).Infof("Response Details") +} + +func (suite *ResponseManagementSuite) TestShouldFailFetchingLibraryWithUnknownName() { + match := func(library gcloudcx.ResponseManagementLibrary) bool { + return library.Name == "unknown library" + } + _, err := gcloudcx.FetchBy(context.Background(), suite.Client, match) + suite.Require().Error(err, "Should have failed to fetch Response Management Library") + suite.Logger.Errorf("Expected Failure", err) + suite.Assert().ErrorIs(err, errors.NotFound, "Should have failed to fetch Response Management Library") +} + +func (suite *ResponseManagementSuite) TestShouldFailFetchingResponseWithUnknownName() { + match := func(response gcloudcx.ResponseManagementResponse) bool { + return response.Name == "unknown response" + } + _, err := gcloudcx.FetchBy(context.Background(), suite.Client, match, gcloudcx.Query{"libraryId": suite.LibraryID}) + suite.Require().Error(err, "Should have failed to fetch Response Management Response") + suite.Logger.Errorf("Expected Failure", err) + suite.Assert().ErrorIs(err, errors.NotFound, "Should have failed to fetch Response Management Response") +} diff --git a/routing_message_recipient.go b/routing_message_recipient.go new file mode 100644 index 0000000..266e040 --- /dev/null +++ b/routing_message_recipient.go @@ -0,0 +1,108 @@ +package gcloudcx + +import ( + "context" + "encoding/json" + "time" + + "github.com/gildas/go-errors" + "github.com/gildas/go-logger" + "github.com/google/uuid" +) + +type RoutingMessageRecipient struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + MessengerType string `json:"messengerType"` + Flow *Flow `json:"flow"` + DateCreated time.Time `json:"dateCreated,omitempty"` + CreatedBy *User `json:"createdBy,omitempty"` + DateModified time.Time `json:"dateModified,omitempty"` + ModifiedBy *User `json:"modifiedBy,omitempty"` + client *Client `json:"-"` + logger *logger.Logger `json:"-"` +} + +// Initialize initializes the object +// +// accepted parameters: *gcloufcx.Client, *logger.Logger +// +// implements Initializable +func (recipient *RoutingMessageRecipient) Initialize(parameters ...interface{}) { + for _, raw := range parameters { + switch parameter := raw.(type) { + case *Client: + recipient.client = parameter + case *logger.Logger: + recipient.logger = parameter.Child("routingmessagerecipient", "routingmessagerecipient", "id", recipient.ID) + } + } +} + +// GetID gets the identifier of this +// +// implements Identifiable +func (recipient RoutingMessageRecipient) GetID() uuid.UUID { + return recipient.ID +} + +// GetURI gets the URI of this +// +// implements Addressable +func (recipient RoutingMessageRecipient) GetURI(ids ...uuid.UUID) URI { + if len(ids) > 0 { + return NewURI("/api/v2/routing/message/recipients/%s", ids[0]) + } + if recipient.ID != uuid.Nil { + return NewURI("/api/v2/routing/message/recipients/%s", recipient.ID) + } + return URI("/api/v2/routing/message/recipients/") +} + +// UpdateFlow updates the flow of this recipient +func (recipient *RoutingMessageRecipient) UpdateFlow(context context.Context, flow *Flow) error { + // Should we disconnect the current flow first? + type FlowRef struct { + ID string `json:"id"` + Name string `json:"name"` + } + + recipient.Flow = flow + return recipient.client.Put( + context, + recipient.GetURI(), + struct { + ID string `json:"id"` + Name string `json:"name"` + MessengerType string `json:"messengerType"` + Flow FlowRef `json:"flow"` + }{ + ID: recipient.ID.String(), + Name: recipient.Name, + MessengerType: recipient.MessengerType, + Flow: FlowRef{ID: recipient.Flow.ID.String(), Name: recipient.Flow.Name}, + }, + nil, + ) +} + +// DeleteFlow removes the flow of this recipient +func (recipient *RoutingMessageRecipient) DeleteFlow(context context.Context) error { + recipient.Flow = nil + return recipient.client.Put(context, recipient.GetURI(), recipient, nil) +} + +// MarshalJSON marshals this into JSON +// +// implements json.Marshaler +func (recipient RoutingMessageRecipient) MarshalJSON() ([]byte, error) { + type surrogate RoutingMessageRecipient + data, err := json.Marshal(&struct { + surrogate + SelfURI URI `json:"selfUri"` + }{ + surrogate: surrogate(recipient), + SelfURI: recipient.GetURI(), + }) + return data, errors.JSONMarshalError.Wrap(err) +} diff --git a/routing_message_recipient_test.go b/routing_message_recipient_test.go new file mode 100644 index 0000000..41f2ec9 --- /dev/null +++ b/routing_message_recipient_test.go @@ -0,0 +1,291 @@ +package gcloudcx_test + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + "time" + + "github.com/gildas/go-core" + "github.com/gildas/go-gcloudcx" + "github.com/gildas/go-logger" + "github.com/google/uuid" + "github.com/joho/godotenv" + "github.com/stretchr/testify/suite" +) + +type RoutingMessageRecipientSuite struct { + suite.Suite + Name string + Logger *logger.Logger + Start time.Time + + IntegrationID uuid.UUID + Client *gcloudcx.Client +} + +func TestRoutingMessageRecipientSuite(t *testing.T) { + suite.Run(t, new(RoutingMessageRecipientSuite)) +} + +// ***************************************************************************** +// #region: Suite Tools {{{ +func (suite *RoutingMessageRecipientSuite) SetupSuite() { + _ = godotenv.Load() + suite.Name = strings.TrimSuffix(reflect.TypeOf(suite).Elem().Name(), "Suite") + suite.Logger = logger.Create("test", + &logger.FileStream{ + Path: fmt.Sprintf("./log/test-%s.log", strings.ToLower(suite.Name)), + Unbuffered: true, + FilterLevel: logger.TRACE, + SourceInfo: true, + }, + ).Child("test", "test") + suite.Logger.Infof("Suite Start: %s %s", suite.Name, strings.Repeat("=", 80-14-len(suite.Name))) + + var ( + region = core.GetEnvAsString("PURECLOUD_REGION", "") + clientID = uuid.MustParse(core.GetEnvAsString("PURECLOUD_CLIENTID", "")) + secret = core.GetEnvAsString("PURECLOUD_CLIENTSECRET", "") + token = core.GetEnvAsString("PURECLOUD_CLIENTTOKEN", "") + deploymentID = uuid.MustParse(core.GetEnvAsString("PURECLOUD_DEPLOYMENTID", "")) + ) + suite.Client = gcloudcx.NewClient(&gcloudcx.ClientOptions{ + Region: region, + DeploymentID: deploymentID, + Logger: suite.Logger, + }).SetAuthorizationGrant(&gcloudcx.ClientCredentialsGrant{ + ClientID: clientID, + Secret: secret, + Token: gcloudcx.AccessToken{ + Type: "bearer", + Token: token, + }, + }) + suite.Require().NotNil(suite.Client, "GCloudCX Client is nil") +} + +func (suite *RoutingMessageRecipientSuite) TearDownSuite() { + if suite.T().Failed() { + suite.Logger.Warnf("At least one test failed, we are not cleaning") + suite.T().Log("At least one test failed, we are not cleaning") + } else { + suite.Logger.Infof("All tests succeeded, we are cleaning") + } + suite.Logger.Infof("Suite End: %s %s", suite.Name, strings.Repeat("=", 80-12-len(suite.Name))) + suite.Logger.Close() +} + +func (suite *RoutingMessageRecipientSuite) BeforeTest(suiteName, testName string) { + suite.Logger.Infof("Test Start: %s %s", testName, strings.Repeat("-", 80-13-len(testName))) + + suite.Start = time.Now() + + // Reuse tokens as much as we can + if !suite.Client.IsAuthorized() { + suite.Logger.Infof("Client is not logged in...") + err := suite.Client.Login(context.Background()) + suite.Require().Nil(err, "Failed to login") + suite.Logger.Infof("Client is now logged in...") + } else { + suite.Logger.Infof("Client is already logged in...") + } +} + +func (suite *RoutingMessageRecipientSuite) AfterTest(suiteName, testName string) { + duration := time.Since(suite.Start) + suite.Logger.Record("duration", duration.String()).Infof("Test End: %s %s", testName, strings.Repeat("-", 80-11-len(testName))) +} + +func (suite *RoutingMessageRecipientSuite) LoadTestData(filename string) []byte { + data, err := os.ReadFile(filepath.Join(".", "testdata", filename)) + suite.Require().NoErrorf(err, "Failed to Load Data. %s", err) + return data +} + +func (suite *RoutingMessageRecipientSuite) UnmarshalData(filename string, v interface{}) error { + data := suite.LoadTestData(filename) + suite.Logger.Infof("Loaded %s: %s", filename, string(data)) + return json.Unmarshal(data, v) +} + +// #endregion: Suite Tools }}} +// ***************************************************************************** + +func (suite *RoutingMessageRecipientSuite) TestCanMarshal() { + expected := suite.LoadTestData("routing-message-recipient.json") + recipient := gcloudcx.RoutingMessageRecipient{ + ID: uuid.MustParse("34071108-1569-4cb0-9137-a326b8a9e815"), + Name: "TEST-GO-PURECLOUD", + MessengerType: "open", + Flow: &gcloudcx.Flow{ + ID: uuid.MustParse("900fa1cb-427b-4ae3-9439-079ac3f07d56"), + Name: "Gildas-TestOpenMessaging", + }, + DateCreated: time.Date(2021, 4, 8, 3, 12, 7, 888000000, time.UTC), + CreatedBy: &gcloudcx.User{ID: uuid.MustParse("3e23b1b3-325f-4fbd-8fe0-e88416850c0e")}, + DateModified: time.Date(2021, 4, 8, 3, 12, 7, 888000000, time.UTC), + ModifiedBy: &gcloudcx.User{ID: uuid.MustParse("2229bd78-a6e4-412f-b789-ef70f447e5db")}, + } + payload, err := json.Marshal(recipient) + suite.Require().NoError(err, "Failed to Marshal") + suite.Require().JSONEq(string(expected), string(payload), "Payload does not match") +} + +func (suite *RoutingMessageRecipientSuite) TestCanUnmarshal() { + var recipient gcloudcx.RoutingMessageRecipient + err := suite.UnmarshalData("routing-message-recipient.json", &recipient) + suite.Require().NoErrorf(err, "Failed to Unmarshal Data. %s", err) + suite.Require().NotNil(recipient, "Recipient is nil") + suite.Require().Implements((*core.Identifiable)(nil), recipient) + suite.Assert().Equal("34071108-1569-4cb0-9137-a326b8a9e815", recipient.ID.String()) + suite.Assert().Equal("TEST-GO-PURECLOUD", recipient.Name) + suite.Assert().Equal("open", recipient.MessengerType) + suite.Assert().Equal("3e23b1b3-325f-4fbd-8fe0-e88416850c0e", recipient.CreatedBy.ID.String()) + suite.Assert().Equal("2021-04-08T03:12:07Z", recipient.DateCreated.Format(time.RFC3339)) + suite.Assert().Equal("2229bd78-a6e4-412f-b789-ef70f447e5db", recipient.ModifiedBy.ID.String()) + suite.Assert().Equal("2021-04-08T03:12:07Z", recipient.DateModified.Format(time.RFC3339)) + suite.Require().Implements((*gcloudcx.Addressable)(nil), recipient) + suite.Assert().Equal(gcloudcx.NewURI("/api/v2/routing/message/recipients/%s", recipient.GetID()), recipient.GetURI()) +} + +func (suite *RoutingMessageRecipientSuite) TestCanFetchByID() { + id := uuid.MustParse("2be1fcc8-4f7d-406d-accc-43be454e2f14") + recipient, err := gcloudcx.Fetch[gcloudcx.RoutingMessageRecipient](context.Background(), suite.Client, id) + suite.Require().NoErrorf(err, "Failed to fetch Routing Message Recipient %s. %s", id, err) + suite.Assert().Equal(id, recipient.GetID()) + suite.Assert().Equal("GILDAS-OpenMessaging Integration Test-Viber", recipient.Name) + suite.Assert().Equal("open", recipient.MessengerType) + suite.Require().NotNil(recipient.Flow, "Recipient should have a Flow") + suite.Assert().Equal("Gildas-TestOpenMessaging", recipient.Flow.Name) +} + +func (suite *RoutingMessageRecipientSuite) TestCanFetchByName() { + name := "GILDAS-OpenMessaging Integration Test-Viber" + match := func(recipient gcloudcx.RoutingMessageRecipient) bool { + return recipient.Name == name + } + recipient, err := gcloudcx.FetchBy(context.Background(), suite.Client, match) + suite.Require().NoErrorf(err, "Failed to fetch Routing Message Recipient %s. %s", name, err) + suite.Assert().Equal(uuid.MustParse("2be1fcc8-4f7d-406d-accc-43be454e2f14"), recipient.GetID()) + suite.Assert().Equal(name, recipient.Name) + suite.Assert().Equal("open", recipient.MessengerType) + suite.Require().NotNil(recipient.Flow, "Recipient should have a Flow") + suite.Assert().Equal("Gildas-TestOpenMessaging", recipient.Flow.Name) +} + +func (suite *RoutingMessageRecipientSuite) TestCanFetchAll() { + recipients, err := gcloudcx.FetchAll[gcloudcx.RoutingMessageRecipient](context.Background(), suite.Client, gcloudcx.Query{"messengerType": "open"}) + suite.Require().NoError(err, "Failed to fetch Routing Message Recipients") + suite.Require().NotEmpty(recipients, "No Routing Message Recipients") + suite.Logger.Infof("Found %d Routing Message Recipients", len(recipients)) + suite.Assert().Greater(len(recipients), 25, "Not enough Routing Message Recipients") + for _, recipient := range recipients { + suite.Logger.Record("recipient", recipient).Infof("Got a Routing Message Recipient") + suite.Assert().NotEmpty(recipient.ID) + suite.Assert().NotEmpty(recipient.Name) + suite.T().Logf("%s => %s", recipient.Name, recipient.Flow) + } +} + +func (suite *RoutingMessageRecipientSuite) TestCanFetchByIntegration() { + webhookURL, _ := url.Parse("https://www.genesys.com/gcloudcx") + webhookToken := "DEADBEEF" + integration, err := suite.Client.CreateOpenMessagingIntegration(context.Background(), "UNITTEST-go-gcloudcx", webhookURL, webhookToken, nil) + suite.Require().NoError(err, "Failed to create integration") + suite.Logger.Record("integration", integration).Infof("Created a integration") + for { + if integration.IsCreated() { + break + } + suite.Logger.Warnf("Integration %s is still in status: %s, waiting a bit", integration.ID, integration.CreateStatus) + time.Sleep(time.Second) + err = integration.Refresh(context.Background()) + suite.Require().NoError(err, "Failed to refresh integration") + } + defer func(integration *gcloudcx.OpenMessagingIntegration) { + if integration != nil && integration.IsCreated() { + err := integration.Delete(context.Background()) + suite.Require().NoError(err, "Failed to delete integration") + } + }(integration) + suite.Logger.Infof("Fetching Recipient for Integration %s", integration.GetID()) + recipient, err := integration.GetRoutingMessageRecipient(context.Background()) + suite.Require().NoErrorf(err, "Failed to fetch Routing Message Recipient %s. %s", integration.GetID(), err) + suite.Assert().Equal(integration.GetID(), recipient.GetID()) + suite.Assert().Nil(recipient.Flow, "Recipient should not have a Flow") + suite.Logger.Record("recipient", recipient).Infof("Got a Routing Message Recipient") +} + +func (suite *RoutingMessageRecipientSuite) TestCanUpdateFlow() { + webhookURL, _ := url.Parse("https://www.genesys.com/gcloudcx") + webhookToken := "DEADBEEF" + integration, err := suite.Client.CreateOpenMessagingIntegration(context.Background(), "UNITTEST-go-gcloudcx", webhookURL, webhookToken, nil) + suite.Require().NoError(err, "Failed to create integration") + suite.Logger.Record("integration", integration).Infof("Created a integration") + for { + if integration.IsCreated() { + break + } + suite.Logger.Warnf("Integration %s is still in status: %s, waiting a bit", integration.ID, integration.CreateStatus) + time.Sleep(time.Second) + err = integration.Refresh(context.Background()) + suite.Require().NoError(err, "Failed to refresh integration") + } + defer func(integration *gcloudcx.OpenMessagingIntegration) { + if integration != nil && integration.IsCreated() { + suite.Logger.Infof("Deleting integration %s", integration.GetID()) + recipient, _ := integration.GetRoutingMessageRecipient(context.Background()) + err := recipient.DeleteFlow(context.Background()) + suite.Require().NoError(err, "Failed to delete flow") + err = integration.Delete(context.Background()) + suite.Require().NoError(err, "Failed to delete integration") + } + }(integration) + suite.Logger.Infof("Fetching Recipient for Integration %s", integration.GetID()) + recipient, err := integration.GetRoutingMessageRecipient(context.Background()) + suite.Require().NoErrorf(err, "Failed to fetch Routing Message Recipient %s. %s", integration.GetID(), err) + suite.Assert().Equal(integration.GetID(), recipient.GetID()) + suite.Assert().Nil(recipient.Flow, "Recipient should not have a Flow") + suite.Logger.Record("recipient", recipient).Infof("Got a Routing Message Recipient") + + flow := gcloudcx.Flow{ + ID: uuid.MustParse(core.GetEnvAsString("FLOW1_ID", "")), + Name: core.GetEnvAsString("FLOW1_NAME", "Flow1"), + IsActive: true, + } + + suite.Logger.Infof("Updating Recipient %s's flow to %s", recipient.GetID(), flow.Name) + err = recipient.UpdateFlow(context.Background(), &flow) + suite.Require().NoErrorf(err, "Failed to update Routing Message Recipient %s. %s", recipient.GetID(), err) + + verify, err := integration.GetRoutingMessageRecipient(context.Background()) + suite.Require().NoErrorf(err, "Failed to fetch Routing Message Recipient %s. %s", integration.GetID(), err) + suite.Assert().Equal(integration.GetID(), verify.GetID()) + suite.Assert().NotNil(verify.Flow, "Recipient should have a Flow") + suite.Assert().Equal(flow.Name, verify.Flow.Name) + suite.Assert().Equal(flow.ID.String(), verify.Flow.ID.String()) + + flow = gcloudcx.Flow{ + ID: uuid.MustParse(core.GetEnvAsString("FLOW2_ID", "")), + Name: core.GetEnvAsString("FLOW2_NAME", "Flow2"), + IsActive: true, + } + + suite.Logger.Infof("Updating Recipient %s's flow to %s", recipient.GetID(), flow.Name) + err = recipient.UpdateFlow(context.Background(), &flow) + suite.Require().NoErrorf(err, "Failed to update Routing Message Recipient %s. %s", recipient.GetID(), err) + + verify, err = integration.GetRoutingMessageRecipient(context.Background()) + suite.Require().NoErrorf(err, "Failed to fetch Routing Message Recipient %s. %s", integration.GetID(), err) + suite.Assert().Equal(integration.GetID(), verify.GetID()) + suite.Assert().NotNil(verify.Flow, "Recipient should have a Flow") + suite.Assert().Equal(flow.Name, verify.Flow.Name) + suite.Assert().Equal(flow.ID.String(), verify.Flow.ID.String()) +} diff --git a/testdata/routing-message-recipient.json b/testdata/routing-message-recipient.json new file mode 100644 index 0000000..30bb720 --- /dev/null +++ b/testdata/routing-message-recipient.json @@ -0,0 +1,24 @@ +{ + "id": "34071108-1569-4cb0-9137-a326b8a9e815", + "name": "TEST-GO-PURECLOUD", + "flow": { + "id": "900fa1cb-427b-4ae3-9439-079ac3f07d56", + "name": "Gildas-TestOpenMessaging", + "active": false, + "system": false, + "deleted": false, + "selfUri": "/api/v2/flows/900fa1cb-427b-4ae3-9439-079ac3f07d56" + }, + "dateCreated": "2021-04-08T03:12:07.888Z", + "dateModified": "2021-04-08T03:12:07.888Z", + "createdBy": { + "id": "3e23b1b3-325f-4fbd-8fe0-e88416850c0e", + "selfUri": "/api/v2/users/3e23b1b3-325f-4fbd-8fe0-e88416850c0e" + }, + "modifiedBy": { + "id": "2229bd78-a6e4-412f-b789-ef70f447e5db", + "selfUri": "/api/v2/users/2229bd78-a6e4-412f-b789-ef70f447e5db" + }, + "messengerType": "open", + "selfUri": "/api/v2/routing/message/recipients/34071108-1569-4cb0-9137-a326b8a9e815" +} diff --git a/testdata/user.json b/testdata/user.json index 0e9ad68..2da3ad9 100644 --- a/testdata/user.json +++ b/testdata/user.json @@ -27,6 +27,5 @@ } ], "version": 29, - "acdAutoAnswer": false, "selfUri": "/api/v2/users/06ffcd2e-1ada-412e-a5f5-30d7853246dd" } diff --git a/types.go b/types.go index b96e9dd..678fff5 100644 --- a/types.go +++ b/types.go @@ -4,6 +4,7 @@ import ( "context" "github.com/gildas/go-core" + "github.com/google/uuid" ) // Identifiable describes that can get their Identifier as a UUID @@ -13,17 +14,25 @@ type Identifiable interface { // Addressable describes things that carry a URI (typically /api/v2/things/{{uuid}}) type Addressable interface { - GetURI() URI + // GetURI gets the URI + // + // if ids are provided, they are used to replace the {{uuid}} in the URI. + // + // if no ids are provided and the Addressable has a UUID, it is used to replace the {{uuid}} in the URI. + // + // else, the pattern for the URI is returned ("/api/v2/things/%s") + GetURI(ids ...uuid.UUID) URI } // Initializable describes things that can be initialized type Initializable interface { - Initialize(parameters ...interface{}) error + Initialize(parameters ...interface{}) } // Fetchable describes things that can be fetched from the Genesys Cloud API type Fetchable interface { - Fetch(context context.Context, client *Client, parameters ...interface{}) error + Identifiable + Addressable } // StateUpdater describes objects than can update the state of an Identifiable diff --git a/uri.go b/uri.go index da782f3..2d27434 100644 --- a/uri.go +++ b/uri.go @@ -8,7 +8,7 @@ import ( "strings" ) -// URI represents the Path of a URL (used in SelfURI, for example) +// URI represents the Path of a URL (used in SelfURI or in requests, for example) type URI string // NewURI creates a new URI with eventually some parameters @@ -18,15 +18,36 @@ func NewURI(path string, args ...interface{}) URI { return URI(fmt.Sprintf(path, args...)) } +// WithQuery returns a new URI with the given query +func (uri URI) WithQuery(query Query) URI { + if len(query) == 0 { + return uri + } + if uri.HasQuery() { + return URI(fmt.Sprintf("%s&%s", uri, query.Encode())) + } + return URI(fmt.Sprintf("%s?%s", uri, query.Encode())) +} + +// HasProtocol tells if the URI starts with a protocol/scheme func (uri URI) HasProtocol() bool { matched, _ := regexp.Match(`^[a-z0-9_]+:.*`, []byte(uri)) return matched } +// HasPrefix tells if the URI starts with the given prefix func (uri URI) HasPrefix(prefix string) bool { return strings.HasPrefix(string(uri), prefix) } +// HasQuery tells if the URI has a query string +func (uri URI) HasQuery() bool { + return strings.Contains(string(uri), "?") +} + +// Join joins the given paths to the URI +// +// Caveat: does not work if the original URI has a query string func (uri URI) Join(uris ...URI) URI { paths := []string{uri.String()} for _, u := range uris { @@ -35,10 +56,14 @@ func (uri URI) Join(uris ...URI) URI { return URI(path.Join(paths...)) } +// URL returns the URI as a URL func (uri URI) URL() (*url.URL, error) { return url.Parse(string(uri)) } +// String returns the URI as a string +// +// implements the fmt.Stringer interface func (uri URI) String() string { return string(uri) } diff --git a/uri_query.go b/uri_query.go new file mode 100644 index 0000000..39781a8 --- /dev/null +++ b/uri_query.go @@ -0,0 +1,18 @@ +package gcloudcx + +import ( + "fmt" + "net/url" +) + +// Query represents a query string for URIs +type Query map[string]interface{} + +// Encode returns the query as a "URL encoded" string +func (query Query) Encode() string { + values := url.Values{} + for key, value := range query { + values.Set(key, fmt.Sprintf("%v", value)) + } + return values.Encode() +} diff --git a/uri_test.go b/uri_test.go index 00fbcf3..bece3c2 100644 --- a/uri_test.go +++ b/uri_test.go @@ -42,3 +42,11 @@ func (suite *URISuite) TestHasProtocol() { suite.Assert().True(gcloudcx.NewURI("https://www.acme.com/api/v2/users/me").HasProtocol()) suite.Assert().False(gcloudcx.NewURI("/users/me").HasProtocol()) } + +func (suite *URISuite) TestCanAddQuery() { + uri := gcloudcx.NewURI("/api/v2/users/me").WithQuery(gcloudcx.Query{"expand": "profile"}) + suite.Assert().Equal("/api/v2/users/me?expand=profile", uri.String()) + + newuri := uri.WithQuery(gcloudcx.Query{"pageNumber": 1}) + suite.Assert().Equal("/api/v2/users/me?expand=profile&pageNumber=1", newuri.String()) +} diff --git a/user.go b/user.go index f05fe04..bd489a1 100644 --- a/user.go +++ b/user.go @@ -2,6 +2,7 @@ package gcloudcx import ( "context" + "encoding/json" "net/url" "strings" @@ -13,9 +14,8 @@ import ( // User describes a GCloud User type User struct { ID uuid.UUID `json:"id"` - SelfURI URI `json:"selfUri"` - Name string `json:"name"` - UserName string `json:"username"` + Name string `json:"name,omitempty"` + UserName string `json:"username,omitempty"` Department string `json:"department,omitempty"` Title string `json:"title,omitempty"` Division *Division `json:"division,omitempty"` @@ -26,7 +26,7 @@ type User struct { State string `json:"state,omitempty"` Presence *UserPresence `json:"presence,omitempty"` OutOfOffice *OutOfOffice `json:"outOfOffice,omitempty"` - AcdAutoAnswer bool `json:"acdAutoAnswer"` + AcdAutoAnswer bool `json:"acdAutoAnswer,omitempty"` RoutingStatus *RoutingStatus `json:"routingStatus,omitempty"` ProfileSkills []string `json:"profileSkills,omitempty"` Skills []*UserRoutingSkill `json:"skills,omitempty"` @@ -43,7 +43,7 @@ type User struct { Locations []*Location `json:"locations,omitempty"` GeoLocation *GeoLocation `json:"geolocation,omitempty"` Chat *Jabber `json:"chat,omitempty"` - Version int `json:"version"` + Version int `json:"version,omitempty"` client *Client `json:"-"` logger *logger.Logger `json:"-"` } @@ -53,34 +53,6 @@ type Jabber struct { ID string `json:"jabberId"` } -// Fetch fetches a user -// -// implements Fetchable -func (user *User) Fetch(ctx context.Context, client *Client, parameters ...interface{}) error { - id, name, selfURI, log := client.ParseParameters(ctx, user, parameters...) - - if id != uuid.Nil { - if err := client.Get(ctx, NewURI("/users/%s", id), &user); err != nil { - return err - } - user.logger = log - } else if len(selfURI) > 0 { - if err := client.Get(ctx, selfURI, &user); err != nil { - return err - } - user.logger = log.Record("id", user.ID) - } else if len(name) > 0 { - return errors.NotImplemented.WithStack() - } else if _, ok := client.Grant.(*ClientCredentialsGrant); !ok { // /users/me is not possible with ClientCredentialsGrant - if err := client.Get(ctx, "/users/me", &user); err != nil { - return err - } - user.logger = log.Record("id", user.ID) - } - user.client = client - return nil -} - // GetMyUser retrieves the User that authenticated with the client // properties is one of more properties that should be expanded // see https://developer.mypurecloud.com/api/rest/v2/users/#get-api-v2-users-me @@ -98,20 +70,45 @@ func (client *Client) GetMyUser(context context.Context, properties ...string) ( return user, nil } +// Initialize initializes the object +// +// accepted parameters: *gcloufcx.Client, *logger.Logger +// +// implements Initializable +func (user *User) Initialize(parameters ...interface{}) { + for _, raw := range parameters { + switch parameter := raw.(type) { + case *Client: + user.client = parameter + case *logger.Logger: + user.logger = parameter.Child("user", "user", "id", user.ID) + } + } +} + // GetID gets the identifier of this -// implements Identifiable +// +// implements Identifiable func (user User) GetID() uuid.UUID { return user.ID } // GetURI gets the URI of this -// implements Addressable -func (user User) GetURI() URI { - return user.SelfURI +// +// implements Addressable +func (user User) GetURI(ids ...uuid.UUID) URI { + if len(ids) > 0 { + return NewURI("/api/v2/users/%s", ids[0]) + } + if user.ID != uuid.Nil { + return NewURI("/api/v2/users/%s", user.ID) + } + return URI("/api/v2/users/") } // String gets a string version -// implements the fmt.Stringer interface +// +// implements the fmt.Stringer interface func (user User) String() string { if len(user.Name) > 0 { return user.Name @@ -124,3 +121,18 @@ func (user User) String() string { } return user.ID.String() } + +// MarshalJSON marshals this into JSON +// +// implements json.Marshaler +func (user User) MarshalJSON() ([]byte, error) { + type surrogate User + data, err := json.Marshal(&struct { + surrogate + SelfURI URI `json:"selfUri"` + }{ + surrogate: surrogate(user), + SelfURI: user.GetURI(), + }) + return data, errors.JSONMarshalError.Wrap(err) +} diff --git a/user_test.go b/user_test.go index 1f05e1a..0fc963d 100644 --- a/user_test.go +++ b/user_test.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" "net/url" + "os" + "path/filepath" "reflect" "strings" "testing" @@ -34,72 +36,14 @@ func TestUserSuite(t *testing.T) { suite.Run(t, new(UserSuite)) } -func (suite *UserSuite) TestCanUnmarshal() { - user := gcloudcx.User{} - err := LoadObject("user.json", &user) - suite.Require().Nil(err, "Failed to unmarshal user. %s", err) - suite.Logger.Record("User", user).Infof("Got a user") - suite.Assert().NotEmpty(user.ID) - suite.Assert().Equal("John Doe", user.Name) -} - -func (suite *UserSuite) TestCanMarshal() { - user := gcloudcx.User{ - ID: uuid.MustParse("06ffcd2e-1ada-412e-a5f5-30d7853246dd"), - Name: "John Doe", - UserName: "john.doe@acme.com", - Mail: "john.doe@acme.com", - Title: "Junior", - Division: &gcloudcx.Division{ - ID: uuid.MustParse("06ffcd2e-1ada-412e-a5f5-30d7853246dd"), - Name: "", - SelfURI: "/api/v2/authorization/divisions/06ffcd2e-1ada-412e-a5f5-30d7853246dd", - }, - Chat: &gcloudcx.Jabber{ - ID: "98765432d220541234567654@genesysapacanz.orgspan.com", - }, - Addresses: []*gcloudcx.Contact{}, - PrimaryContact: []*gcloudcx.Contact{ - { - Type: "PRIMARY", - MediaType: "EMAIL", - Address: "john.doe@acme.com", - }, - }, - Images: []*gcloudcx.UserImage{ - { - Resolution: "x96", - ImageURL: core.Must(url.Parse("https://prod-apse2-inin-directory-service-profile.s3-ap-southeast-2.amazonaws.com/7fac0a12/4643/4d0e/86f3/2467894311b5.jpg")).(*url.URL), - }, - }, - AcdAutoAnswer: false, - State: "active", - SelfURI: "/api/v2/users/06ffcd2e-1ada-412e-a5f5-30d7853246dd", - Version: 29, - } - - data, err := json.Marshal(user) - suite.Require().Nil(err, "Failed to marshal User. %s", err) - expected, err := LoadFile("user.json") - suite.Require().Nil(err, "Failed to Load Data. %s", err) - suite.Assert().JSONEq(string(expected), string(data)) -} - -func (suite *UserSuite) TestCanFetchByID() { - user := gcloudcx.User{} - err := suite.Client.Fetch(context.Background(), &user, suite.UserID) - suite.Require().Nilf(err, "Failed to fetch User %s. %s", suite.UserID, err) - suite.Assert().Equal(suite.UserName, user.Name) -} - -// Suite Tools - +// ***************************************************************************** +// #region: Suite Tools {{{ func (suite *UserSuite) SetupSuite() { var err error var value string _ = godotenv.Load() - suite.Name = strings.TrimSuffix(reflect.TypeOf(*suite).Name(), "Suite") + suite.Name = strings.TrimSuffix(reflect.TypeOf(suite).Elem().Name(), "Suite") suite.Logger = logger.Create("test", &logger.FileStream{ Path: fmt.Sprintf("./log/test-%s.log", strings.ToLower(suite.Name)), @@ -119,7 +63,7 @@ func (suite *UserSuite) SetupSuite() { suite.Require().NotEmpty(value, "USER_ID is not set in your environment") suite.UserID, err = uuid.Parse(value) - suite.Require().Nil(err, "USER_ID is not a valid UUID") + suite.Require().NoError(err, "USER_ID is not a valid UUID") suite.UserName = core.GetEnvAsString("USER_NAME", "") suite.Require().NotEmpty(suite.UserName, "USER_NAME is not set in your environment") @@ -153,7 +97,7 @@ func (suite *UserSuite) BeforeTest(suiteName, testName string) { if !suite.Client.IsAuthorized() { suite.Logger.Infof("Client is not logged in...") err := suite.Client.Login(context.Background()) - suite.Require().Nil(err, "Failed to login") + suite.Require().NoError(err, "Failed to login") suite.Logger.Infof("Client is now logged in...") } else { suite.Logger.Infof("Client is already logged in...") @@ -164,3 +108,84 @@ func (suite *UserSuite) AfterTest(suiteName, testName string) { duration := time.Since(suite.Start) suite.Logger.Record("duration", duration.String()).Infof("Test End: %s %s", testName, strings.Repeat("-", 80-11-len(testName))) } + +func (suite *UserSuite) LoadTestData(filename string) []byte { + data, err := os.ReadFile(filepath.Join(".", "testdata", filename)) + suite.Require().NoErrorf(err, "Failed to Load Data. %s", err) + return data +} + +func (suite *UserSuite) UnmarshalData(filename string, v interface{}) error { + data := suite.LoadTestData(filename) + suite.Logger.Infof("Loaded %s: %s", filename, string(data)) + return json.Unmarshal(data, v) +} + +// #endregion: Suite Tools }}} +// ***************************************************************************** + +func (suite *UserSuite) TestCanUnmarshal() { + user := gcloudcx.User{} + err := suite.UnmarshalData("user.json", &user) + suite.Require().NoErrorf(err, "Failed to unmarshal user. %s", err) + suite.Logger.Record("User", user).Infof("Got a user") + suite.Assert().NotEmpty(user.ID) + suite.Assert().Equal("John Doe", user.Name) +} + +func (suite *UserSuite) TestCanMarshal() { + user := gcloudcx.User{ + ID: uuid.MustParse("06ffcd2e-1ada-412e-a5f5-30d7853246dd"), + Name: "John Doe", + UserName: "john.doe@acme.com", + Mail: "john.doe@acme.com", + Title: "Junior", + Division: &gcloudcx.Division{ + ID: uuid.MustParse("06ffcd2e-1ada-412e-a5f5-30d7853246dd"), + Name: "", + SelfURI: "/api/v2/authorization/divisions/06ffcd2e-1ada-412e-a5f5-30d7853246dd", + }, + Chat: &gcloudcx.Jabber{ + ID: "98765432d220541234567654@genesysapacanz.orgspan.com", + }, + Addresses: []*gcloudcx.Contact{}, + PrimaryContact: []*gcloudcx.Contact{ + { + Type: "PRIMARY", + MediaType: "EMAIL", + Address: "john.doe@acme.com", + }, + }, + Images: []*gcloudcx.UserImage{ + { + Resolution: "x96", + ImageURL: core.Must(url.Parse("https://prod-apse2-inin-directory-service-profile.s3-ap-southeast-2.amazonaws.com/7fac0a12/4643/4d0e/86f3/2467894311b5.jpg")).(*url.URL), + }, + }, + AcdAutoAnswer: false, + State: "active", + Version: 29, + } + + data, err := json.Marshal(user) + suite.Require().NoErrorf(err, "Failed to marshal User. %s", err) + expected := suite.LoadTestData("user.json") + suite.Assert().JSONEq(string(expected), string(data)) +} + +func (suite *UserSuite) TestCanFetchByID() { + user, err := gcloudcx.Fetch[gcloudcx.User](context.Background(), suite.Client, suite.UserID) + suite.Require().NoErrorf(err, "Failed to fetch User %s. %s", suite.UserID, err) + suite.Assert().Equal(suite.UserID, user.ID) + suite.Assert().Equal(suite.UserName, user.Name) +} + +func (suite *UserSuite) TestCanFetchByName() { + match := func(user gcloudcx.User) bool { + return user.Name == suite.UserName + } + user, err := gcloudcx.FetchBy(context.Background(), suite.Client, match) + suite.Require().NoErrorf(err, "Failed to fetch User %s. %s", suite.UserName, err) + suite.Assert().Equal(suite.UserID, user.ID) + suite.Assert().Equal(suite.UserName, user.Name) +} diff --git a/version.go b/version.go index 10b5a2f..a127c8f 100644 --- a/version.go +++ b/version.go @@ -4,7 +4,7 @@ package gcloudcx var commit string // VERSION is the version of this application -var VERSION = "0.7.14" + commit +var VERSION = "0.7.15" + commit // APP is the name of the application const APP string = "GCloudCX Client"