diff --git a/api_keys.go b/api_keys.go new file mode 100644 index 0000000..8875175 --- /dev/null +++ b/api_keys.go @@ -0,0 +1,57 @@ +package flagsmithapi + +import ( + "fmt" +) + +func (c *Client) GetServerSideEnvKeys(environmentKey string) ([]ServerSideEnvKey, error) { + url := fmt.Sprintf("%s/environments/%s/api-keys/", c.baseURL, environmentKey) + keys := []ServerSideEnvKey{} + resp, err := c.client.R().SetResult(&keys).Get(url) + if err != nil { + return nil, err + } + + if !resp.IsSuccess() { + return nil, fmt.Errorf("flagsmithapi: Error fetching server side keys: %v", resp) + } + return keys, nil +} +func (c *Client) CreateServerSideEnvKey(environmentKey string, key *ServerSideEnvKey) error { + url := fmt.Sprintf("%s/environments/%s/api-keys/", c.baseURL, environmentKey) + + resp, err := c.client.R().SetBody(key).SetResult(&key).Post(url) + if err != nil { + return err + } + if !resp.IsSuccess() { + return fmt.Errorf("flagsmithapi: Error creating server side environment key: %s", resp) + } + return nil + +} +func (c *Client) UpdateServerSideEnvKey(environmentKey string, key *ServerSideEnvKey) error { + url := fmt.Sprintf("%s/environments/%s/api-keys/%d/", c.baseURL, environmentKey, key.ID) + resp, err := c.client.R().SetBody(key).SetResult(&key).Put(url) + if err != nil { + return nil + } + + if !resp.IsSuccess() { + return fmt.Errorf("flagsmithapi: Error updating server side environment key: %s", resp) + } + return nil +} +func (c *Client) DeleteServerSideEnvKey(environmentKey string, keyID int64) error { + url := fmt.Sprintf("%s/environments/%s/api-keys/%d/", c.baseURL, environmentKey, keyID) + resp, err := c.client.R().Delete(url) + if err != nil { + return err + } + + if !resp.IsSuccess() { + return fmt.Errorf("flagsmithapi: Error deleting server side environment key: %s", resp) + } + return nil + +} diff --git a/api_keys_test.go b/api_keys_test.go new file mode 100644 index 0000000..cb315bf --- /dev/null +++ b/api_keys_test.go @@ -0,0 +1,193 @@ +package flagsmithapi_test + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + flagsmithapi "github.com/Flagsmith/flagsmith-go-api-client" +) + +const GetAPIKeysResponseJson = ` +[ + { + "id": 1, + "key": "ser.UiYoRr6zUjiFBUXaRwo7b5", + "active": true, + "created_at": "2022-02-16T12:09:30.349955Z", + "name": "key1", + "expires_at": null + }, + { + "id": 2, + "key": "ser.g5N5Q4L8E832cA3iU4u4td", + "active": true, + "created_at": "2022-02-16T12:09:21.300028Z", + "name": "key2", + "expires_at": null + } +] +` +const KeyOneID int64 = 1 +const KeyTwoID int64 = 2 +const KeyOneName = "key1" +const KeyTwoName = "key2" +const KeyOneKey = "ser.UiYoRr6zUjiFBUXaRwo7b5" +const KeyTwoKey = "ser.g5N5Q4L8E832cA3iU4u4td" + +const CreateAPIKeyResponseJson = ` +{ + "id": 1, + "key": "ser.UiYoRr6zUjiFBUXaRwo7b5", + "active": true, + "created_at": "2024-04-16T07:53:50.808415Z", + "name": "key1", + "expires_at": null +} +` + +func TestGetServerSideEnvKeys(t *testing.T) { + // Given + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, fmt.Sprintf("/api/v1/environments/%s/api-keys/", EnvironmentAPIKey), req.URL.Path) + assert.Equal(t, "GET", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + + rw.Header().Set("Content-Type", "application/json") + _, err := io.WriteString(rw, GetAPIKeysResponseJson) + assert.NoError(t, err) + })) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + keys, err := client.GetServerSideEnvKeys(EnvironmentAPIKey) + + // Then + assert.NoError(t, err) + assert.Len(t, keys, 2) + + // Check the first key + assert.Equal(t, KeyOneID, (keys)[0].ID) + assert.Equal(t, KeyOneName, (keys)[0].Name) + assert.Equal(t, KeyOneKey, (keys)[0].Key) + assert.True(t, (keys)[0].Active) + assert.Equal(t, "2022-02-16T12:09:30.349955Z", (keys)[0].CreatedAt.Format(time.RFC3339Nano)) + assert.Nil(t, (keys)[0].ExpiresAt) + + // Check the second key + assert.Equal(t, KeyTwoID, (keys)[1].ID) + assert.Equal(t, KeyTwoName, (keys)[1].Name) + assert.Equal(t, KeyTwoKey, (keys)[1].Key) + assert.True(t, (keys)[1].Active) + assert.Equal(t, "2022-02-16T12:09:21.300028Z", (keys)[1].CreatedAt.Format(time.RFC3339Nano)) + assert.Nil(t, (keys)[1].ExpiresAt) + +} + +func TestCreateServerSideEnvKey(t *testing.T) { + // Given + expectedRequestBody := `{"active":true,"name":"` + KeyOneName + `"}` + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, fmt.Sprintf("/api/v1/environments/%s/api-keys/", EnvironmentAPIKey), req.URL.Path) + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + + // Test that we sent the correct body + rawBody, err := io.ReadAll(req.Body) + assert.NoError(t, err) + assert.Equal(t, expectedRequestBody, string(rawBody)) + + rw.Header().Set("Content-Type", "application/json") + _, err = io.WriteString(rw, CreateAPIKeyResponseJson) + assert.NoError(t, err) + })) + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + key := flagsmithapi.ServerSideEnvKey{ + Active: true, + Name: KeyOneName, + } + err := client.CreateServerSideEnvKey(EnvironmentAPIKey, &key) + + // Then + assert.NoError(t, err) + assert.Equal(t, int64(KeyOneID), key.ID) + assert.Equal(t, true, key.Active) + assert.Equal(t, KeyOneName, key.Name) +} + +func TestUpdateServerSideEnvKey(t *testing.T) { + // Given + expectedRequestBody := fmt.Sprintf(`{"id":%d,"active":false,"name":"%s"}`, KeyOneID, KeyOneName) + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, fmt.Sprintf("/api/v1/environments/%s/api-keys/%d/", EnvironmentAPIKey, KeyOneID), req.URL.Path) + assert.Equal(t, "PUT", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + + // Test that we sent the correct body + rawBody, err := io.ReadAll(req.Body) + assert.NoError(t, err) + assert.Equal(t, expectedRequestBody, string(rawBody)) + + rw.Header().Set("Content-Type", "application/json") + _, err = io.WriteString(rw, fmt.Sprintf(`{"id":%d,"active":false,"name":"%s"}`, KeyOneID, KeyOneName)) + assert.NoError(t, err) + })) + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + key := flagsmithapi.ServerSideEnvKey{ + ID: KeyOneID, + Active: false, + Name: KeyOneName, + } + err := client.UpdateServerSideEnvKey(EnvironmentAPIKey, &key) + + // Then + assert.NoError(t, err) + assert.Equal(t, int64(KeyOneID), key.ID) + assert.Equal(t, false, key.Active) + assert.Equal(t, KeyOneName, key.Name) +} + +func TestDeleteServerSideEnvKey(t *testing.T) { + // Given + requestReceived := struct { + mu sync.Mutex + isRequestReceived bool + }{} + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + requestReceived.mu.Lock() + requestReceived.isRequestReceived = true + requestReceived.mu.Unlock() + + assert.Equal(t, fmt.Sprintf("/api/v1/environments/%s/api-keys/%d/", EnvironmentAPIKey, KeyOneID), req.URL.Path) + assert.Equal(t, "DELETE", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + })) + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + err := client.DeleteServerSideEnvKey(EnvironmentAPIKey, KeyOneID) + + // Then + requestReceived.mu.Lock() + assert.True(t, requestReceived.isRequestReceived) + requestReceived.mu.Unlock() + assert.NoError(t, err) +} diff --git a/client.go b/client.go index 31aa6de..e0bce54 100644 --- a/client.go +++ b/client.go @@ -111,38 +111,6 @@ func (c *Client) UpdateFeatureState(featureState *FeatureState, updateSegmentPri return nil } -func (c *Client) GetProject(projectUUID string) (*Project, error) { - url := fmt.Sprintf("%s/projects/get-by-uuid/%s/", c.baseURL, projectUUID) - project := Project{} - resp, err := c.client.R(). - SetResult(&project). - Get(url) - - if err != nil { - return nil, err - } - if !resp.IsSuccess() { - return nil, fmt.Errorf("flagsmithapi: Error getting project: %s", resp) - } - return &project, nil - -} -func (c *Client) GetProjectByID(projectID int64) (*Project, error) { - url := fmt.Sprintf("%s/projects/%d/", c.baseURL, projectID) - project := Project{} - resp, err := c.client.R(). - SetResult(&project). - Get(url) - - if err != nil { - return nil, err - } - if !resp.IsSuccess() { - return nil, fmt.Errorf("flagsmithapi: Error getting project: %s", resp) - } - return &project, nil - -} func (c *Client) GetFeature(featureUUID string) (*Feature, error) { url := fmt.Sprintf("%s/features/get-by-uuid/%s/", c.baseURL, featureUUID) feature := Feature{} @@ -434,23 +402,6 @@ func (c *Client) UpdateSegment(segment *Segment) error { return nil } -func (c *Client) GetEnvironment(apiKey string) (*Environment, error) { - url := fmt.Sprintf("%s/environments/%s/", c.baseURL, apiKey) - environment := Environment{} - resp, err := c.client.R(). - SetResult(&environment).Get(url) - - if err != nil { - return nil, err - } - - if !resp.IsSuccess() { - return nil, fmt.Errorf("flagsmithapi: Error getting environment: %s", resp) - } - - return &environment, nil -} - func (c *Client) GetFeatureSegmentByID(featureSegmentID int64) (*FeatureSegment, error) { url := fmt.Sprintf("%s/features/feature-segments/%d/", c.baseURL, featureSegmentID) featureSegment := FeatureSegment{} diff --git a/client_test.go b/client_test.go index 9f84586..f1d3c71 100644 --- a/client_test.go +++ b/client_test.go @@ -239,32 +239,8 @@ const GetProjectResponseJson = ` ` const ProjectID int64 = 10 const ProjectUUID = "cba035f8-d801-416f-a985-ce6e05acbe13" - -func TestGetProject(t *testing.T) { - // Given - server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - assert.Equal(t, fmt.Sprintf("/api/v1/projects/get-by-uuid/%s/", ProjectUUID), req.URL.Path) - assert.Equal(t, "GET", req.Method) - assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) - - rw.Header().Set("Content-Type", "application/json") - _, err := io.WriteString(rw, GetProjectResponseJson) - assert.NoError(t, err) - })) - defer server.Close() - - client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") - - // When - project, err := client.GetProject(ProjectUUID) - - // Then - assert.NoError(t, err) - - assert.Equal(t, ProjectID, project.ID) - assert.Equal(t, ProjectUUID, project.UUID) - assert.Equal(t, "project-1", project.Name) -} +const ProjectName = "project-1" +const OrganisationID = 10 const CreateFeatureResponseJson = ` { @@ -1307,38 +1283,14 @@ const EnvironmentJson = `{ "description": null, "project": 10, "minimum_change_request_approvals": 0, - "allow_client_traits": true + "allow_client_traits": true, + "banner_colour": null, + "banner_text": null, + "hide_disabled_flags": null, + "hide_sensitive_data": false, + "use_identity_composite_key_for_hashing": true }` -func TestGetEnvironment(t *testing.T) { - // Given - server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - assert.Equal(t, fmt.Sprintf("/api/v1/environments/%s/", EnvironmentAPIKey), req.URL.Path) - assert.Equal(t, "GET", req.Method) - assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) - - rw.Header().Set("Content-Type", "application/json") - _, err := io.WriteString(rw, EnvironmentJson) - assert.NoError(t, err) - })) - defer server.Close() - - client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") - - // When - environment, err := client.GetEnvironment(EnvironmentAPIKey) - - // Then - // assert that we did not receive an error - assert.NoError(t, err) - - // assert that the environment is as expected - assert.Equal(t, EnvironmentID, environment.ID) - assert.Equal(t, "Development", environment.Name) - assert.Equal(t, EnvironmentAPIKey, environment.APIKey) - assert.Equal(t, ProjectID, environment.Project) -} - // 400 is arbitrarily chosen to avoid collision with other ids const FeatureSegmentID = int64(400) const GetFeatureSegmentJson = `{ diff --git a/environment.go b/environment.go new file mode 100644 index 0000000..961d621 --- /dev/null +++ b/environment.go @@ -0,0 +1,64 @@ +package flagsmithapi + +import ( + "fmt" +) + +func (c *Client) GetEnvironment(apiKey string) (*Environment, error) { + url := fmt.Sprintf("%s/environments/%s/", c.baseURL, apiKey) + environment := Environment{} + resp, err := c.client.R(). + SetResult(&environment).Get(url) + + if err != nil { + return nil, err + } + + if !resp.IsSuccess() { + return nil, fmt.Errorf("flagsmithapi: Error getting environment: %s", resp) + } + + return &environment, nil +} +func (c *Client) CreateEnvironment(environment *Environment) error { + url := fmt.Sprintf("%s/environments/", c.baseURL) + resp, err := c.client.R().SetBody(environment).SetResult(environment).Post(url) + + if err != nil { + return err + } + + if !resp.IsSuccess() { + return fmt.Errorf("flagsmithapi: Error creating environment: %s", resp) + } + + return nil +} +func (c *Client) UpdateEnvironment(environment *Environment) error { + url := fmt.Sprintf("%s/environments/%s/", c.baseURL, environment.APIKey) + resp, err := c.client.R().SetBody(environment).SetResult(environment).Put(url) + + if err != nil { + return err + } + + if !resp.IsSuccess() { + return fmt.Errorf("flagsmithapi: Error creating environment: %s", resp) + } + + return nil +} +func (c *Client) DeleteEnvironment(apiKey string) error { + url := fmt.Sprintf("%s/environments/%s/", c.baseURL, apiKey) + + resp, err := c.client.R().Delete(url) + if err != nil { + return err + } + + if !resp.IsSuccess() { + return fmt.Errorf("flagsmithapi: Error deleting environment: %s", resp) + } + + return nil +} diff --git a/environment_test.go b/environment_test.go new file mode 100644 index 0000000..6a957fe --- /dev/null +++ b/environment_test.go @@ -0,0 +1,168 @@ +package flagsmithapi_test + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "sync" + + "testing" + + "github.com/stretchr/testify/assert" + + flagsmithapi "github.com/Flagsmith/flagsmith-go-api-client" +) + +const EnvironmentName = "Development" + +func TestGetEnvironment(t *testing.T) { + // Given + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, fmt.Sprintf("/api/v1/environments/%s/", EnvironmentAPIKey), req.URL.Path) + assert.Equal(t, "GET", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + + rw.Header().Set("Content-Type", "application/json") + _, err := io.WriteString(rw, EnvironmentJson) + assert.NoError(t, err) + })) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + environment, err := client.GetEnvironment(EnvironmentAPIKey) + + // Then + // assert that we did not receive an error + assert.NoError(t, err) + + // assert that the environment is as expected + assert.Equal(t, EnvironmentID, environment.ID) + assert.Equal(t, "Development", environment.Name) + assert.Equal(t, EnvironmentAPIKey, environment.APIKey) + assert.Equal(t, ProjectID, environment.Project) +} +func TestCreateEnvironment(t *testing.T) { + // Given + expectedRequestBody := fmt.Sprintf(`{ + "name": "%s", + "description": "This is a test environment", + "project": %d + }`, EnvironmentName, ProjectID) + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, "/api/v1/environments/", req.URL.Path) + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + + // Test that we sent the correct body + rawBody, err := io.ReadAll(req.Body) + assert.NoError(t, err) + assert.JSONEq(t, expectedRequestBody, string(rawBody)) + + rw.Header().Set("Content-Type", "application/json") + _, err = io.WriteString(rw, EnvironmentJson) + assert.NoError(t, err) + })) + + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + environment := &flagsmithapi.Environment{ + Name: EnvironmentName, + Description: "This is a test environment", + Project: ProjectID, + } + err := client.CreateEnvironment(environment) + + // Then + // assert that we did not receive an error + assert.NoError(t, err) + + // assert that the environment is as expected + assert.Equal(t, EnvironmentID, environment.ID) + assert.Equal(t, EnvironmentName, environment.Name) + assert.Equal(t, "environment_api_key", environment.APIKey) +} + +func TestUpdateEnvironment(t *testing.T) { + // Given + updatedDescription := "Updated environment description" + expectedRequestBody := fmt.Sprintf(`{ + "id": %d, + "name": "%s", + "description": "%s", + "project": %d, + "api_key": "%s" + }`, EnvironmentID, EnvironmentName, updatedDescription, ProjectID, EnvironmentAPIKey) + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, fmt.Sprintf("/api/v1/environments/%s/", EnvironmentAPIKey), req.URL.Path) + assert.Equal(t, "PUT", req.Method) + assert.Equal(t, "Api-Key "+EnvironmentAPIKey, req.Header.Get("Authorization")) + + // Test that we sent the correct body + rawBody, err := io.ReadAll(req.Body) + assert.NoError(t, err) + assert.JSONEq(t, expectedRequestBody, string(rawBody)) + + rw.Header().Set("Content-Type", "application/json") + _, err = io.WriteString(rw, fmt.Sprintf(`{"id": 100, "name": "%s", "description": "%s", "api_key": "%s"}`, EnvironmentName, updatedDescription, EnvironmentAPIKey)) + assert.NoError(t, err) + })) + + defer server.Close() + + client := flagsmithapi.NewClient(EnvironmentAPIKey, server.URL+"/api/v1") + + // When + environment := &flagsmithapi.Environment{ + ID: EnvironmentID, + Name: EnvironmentName, + Description: "Updated environment description", + Project: ProjectID, + APIKey: EnvironmentAPIKey, + } + err := client.UpdateEnvironment(environment) + + // Then + // assert that we did not receive an error + assert.NoError(t, err) + + // assert that the environment is as expected + assert.Equal(t, EnvironmentID, environment.ID) + assert.Equal(t, EnvironmentName, environment.Name) + assert.Equal(t, EnvironmentAPIKey, environment.APIKey) +} + +func TestDeleteEnvironment(t *testing.T) { + // Given + requestReceived := struct { + mu sync.Mutex + isRequestReceived bool + }{} + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + requestReceived.mu.Lock() + requestReceived.isRequestReceived = true + requestReceived.mu.Unlock() + + assert.Equal(t, fmt.Sprintf("/api/v1/environments/%s/", EnvironmentAPIKey), req.URL.Path) + assert.Equal(t, "DELETE", req.Method) + assert.Equal(t, "Api-Key "+EnvironmentAPIKey, req.Header.Get("Authorization")) + })) + + client := flagsmithapi.NewClient(EnvironmentAPIKey, server.URL+"/api/v1") + + // When + err := client.DeleteEnvironment(EnvironmentAPIKey) + + // Then + requestReceived.mu.Lock() + assert.True(t, requestReceived.isRequestReceived) + assert.NoError(t, err) +} diff --git a/identity.go b/identity.go new file mode 100644 index 0000000..4e527ff --- /dev/null +++ b/identity.go @@ -0,0 +1,114 @@ +package flagsmithapi + +import ( + "fmt" +) + +func (c *Client) GetIdentity(environmentKey string, identityID int64) (*Identity, error) { + url := fmt.Sprintf("%s/environments/%s/identities/%d/", c.baseURL, environmentKey, identityID) + identity := Identity{} + resp, err := c.client.R().SetResult(&identity).Get(url) + if err != nil { + return nil, err + } + + if !resp.IsSuccess() { + return nil, fmt.Errorf("flagsmithapi: Error fetching identity: %v", resp) + } + return &identity, nil +} +func (c *Client) CreateIdentity(environmentKey string, identity *Identity) error { + url := fmt.Sprintf("%s/environments/%s/identities/", c.baseURL, environmentKey) + + resp, err := c.client.R().SetBody(identity).SetResult(&identity).Post(url) + if err != nil { + return err + } + if !resp.IsSuccess() { + return fmt.Errorf("flagsmithapi: Error creating identity: %s", resp) + } + return nil + +} +func (c *Client) DeleteIdentity(environmentKey string, identityID int64) error { + url := fmt.Sprintf("%s/environments/%s/identities/%d/", c.baseURL, environmentKey, identityID) + resp, err := c.client.R().Delete(url) + if err != nil { + return err + } + + if !resp.IsSuccess() { + return fmt.Errorf("flagsmithapi: Error deleting identity: %s", resp) + } + return nil + +} +func (c *Client) GetTraits(environmentKey string, identityID int64) ([]Trait, error) { + url := fmt.Sprintf("%s/environments/%s/identities/%d/traits/", c.baseURL, environmentKey, identityID) + result := struct { + Traits []Trait `json:"results"` + }{} + resp, err := c.client.R().SetResult(&result).Get(url) + if err != nil { + return nil, err + } + + if !resp.IsSuccess() { + return nil, fmt.Errorf("flagsmithapi: Error fetching traits: %v", resp) + } + return result.Traits, nil +} + +func (c *Client) CreateTrait(environmentKey string, identityID int64, trait *Trait) error { + url := fmt.Sprintf("%s/environments/%s/identities/%d/traits/", c.baseURL, environmentKey, identityID) + + resp, err := c.client.R(). + SetBody(trait). + SetResult(&trait). + Post(url) + + if err != nil { + return err + } + + if !resp.IsSuccess() { + return fmt.Errorf("flagsmithapi: Error creating trait: %v", resp) + } + + return nil +} + +func (c *Client) UpdateTrait(environmentKey string, identityID int64, trait *Trait) error { + url := fmt.Sprintf("%s/environments/%s/identities/%d/traits/%d/", c.baseURL, environmentKey, identityID, trait.ID) + + resp, err := c.client.R(). + SetBody(trait). + Put(url) + + if err != nil { + return err + } + + if !resp.IsSuccess() { + return fmt.Errorf("flagsmithapi: Error updating trait: %v", resp) + } + + return nil +} + +func (c *Client) DeleteTrait(environmentKey string, identityID int64, traitID int64) error { + url := fmt.Sprintf("%s/environments/%s/identities/%d/traits/%d/", c.baseURL, environmentKey, identityID, traitID) + + resp, err := c.client.R(). + Delete(url) + + if err != nil { + return err + } + + if !resp.IsSuccess() { + return fmt.Errorf("flagsmithapi: Error deleting trait: %v", resp) + } + + return nil +} diff --git a/identity_test.go b/identity_test.go new file mode 100644 index 0000000..1cefe2b --- /dev/null +++ b/identity_test.go @@ -0,0 +1,284 @@ +package flagsmithapi_test + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + + flagsmithapi "github.com/Flagsmith/flagsmith-go-api-client" +) + +const IdentityIdentifier = "test-user" +const IdentityID int64 = 1 +const IdentityResponseJson = ` +{ + "id": 1, + "identifier": "test-user" +} +` + +func TestGetIdentity(t *testing.T) { + // Given + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, fmt.Sprintf("/api/v1/environments/%s/identities/%d/", EnvironmentAPIKey, IdentityID), req.URL.Path) + assert.Equal(t, "GET", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + + rw.Header().Set("Content-Type", "application/json") + _, err := io.WriteString(rw, IdentityResponseJson) + assert.NoError(t, err) + })) + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + identity, err := client.GetIdentity(EnvironmentAPIKey, IdentityID) + + // Then + assert.NoError(t, err) + assert.Equal(t, IdentityID, *identity.ID) + assert.Equal(t, IdentityIdentifier, identity.Identifier) +} + +func TestCreateIdentity(t *testing.T) { + // Given + expectedRequestBody := fmt.Sprintf(`{"identifier":"%s"}`, IdentityIdentifier) + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, fmt.Sprintf("/api/v1/environments/%s/identities/", EnvironmentAPIKey), req.URL.Path) + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + + // Test that we sent the correct body + rawBody, err := io.ReadAll(req.Body) + assert.NoError(t, err) + assert.Equal(t, expectedRequestBody, string(rawBody)) + + rw.Header().Set("Content-Type", "application/json") + _, err = io.WriteString(rw, IdentityResponseJson) + assert.NoError(t, err) + })) + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + identity := &flagsmithapi.Identity{ + Identifier: IdentityIdentifier, + } + err := client.CreateIdentity(EnvironmentAPIKey, identity) + + // Then + assert.NoError(t, err) + assert.Equal(t, IdentityID, *identity.ID) + assert.Equal(t, IdentityIdentifier, identity.Identifier) +} +func TestDeleteIdentity(t *testing.T) { + // Given + requestReceived := struct { + mu sync.Mutex + isRequestReceived bool + }{} + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + requestReceived.mu.Lock() + requestReceived.isRequestReceived = true + requestReceived.mu.Unlock() + + assert.Equal(t, fmt.Sprintf("/api/v1/environments/%s/identities/%d/", EnvironmentAPIKey, IdentityID), req.URL.Path) + assert.Equal(t, "DELETE", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + + })) + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + err := client.DeleteIdentity(EnvironmentAPIKey, IdentityID) + + // Then + requestReceived.mu.Lock() + assert.True(t, requestReceived.isRequestReceived) + requestReceived.mu.Unlock() + assert.NoError(t, err) +} + +const GetTraitsJsonResponse = ` +{ + "results": [ + { + "id": 1, + "trait_key": "trait_key_1", + "value_type": "unicode", + "integer_value": null, + "string_value": "value1", + "boolean_value": null, + "float_value": null, + "created_date": "2022-02-04T03:34:31.329637Z" + }, + { + "id": 2, + "trait_key": "trait_key_2", + "value_type": "unicode", + "integer_value": null, + "string_value": "value2", + "boolean_value": null, + "float_value": null, + "created_date": "2022-09-19T08:41:26.542560Z" + } + ] +}` + +func TestGetTraits(t *testing.T) { + // Given + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, fmt.Sprintf("/api/v1/environments/%s/identities/%d/traits/", EnvironmentAPIKey, IdentityID), req.URL.Path) + assert.Equal(t, "GET", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + + rw.Header().Set("Content-Type", "application/json") + _, err := rw.Write([]byte(GetTraitsJsonResponse)) + assert.NoError(t, err) + })) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + traits, err := client.GetTraits(EnvironmentAPIKey, IdentityID) + + // Then + assert.NoError(t, err) + assert.Len(t, traits, 2) + + // Check the first trait + assert.Equal(t, int64(1), traits[0].ID) + assert.Equal(t, "trait_key_1", traits[0].TraitKey) + assert.Equal(t, "unicode", traits[0].ValueType) + assert.Equal(t, "value1", *traits[0].StringValue) + + // Check the second trait + assert.Equal(t, int64(2), traits[1].ID) + assert.Equal(t, "trait_key_2", traits[1].TraitKey) + assert.Equal(t, "unicode", traits[1].ValueType) + assert.Equal(t, "value2", *traits[1].StringValue) +} + +const TraitResponseJson = ` +{ + "id": 1, + "trait_key": "key1", + "value_type": "unicode", + "integer_value": null, + "string_value": "value1", + "boolean_value": null, + "float_value": null, + "created_date": "2024-04-17T07:09:04.385701Z" +} +` + +func TestCreateTrait(t *testing.T) { + // Given + trait_value := "value1" + trait := &flagsmithapi.Trait{ + TraitKey: "key1", + ValueType: "unicode", + StringValue: &trait_value, + } + expectedRequestBody := `{"trait_key":"key1","value_type":"unicode","string_value":"value1"}` + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, fmt.Sprintf("/api/v1/environments/%s/identities/%d/traits/", EnvironmentAPIKey, IdentityID), req.URL.Path) + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + + // Test that we sent the correct body + rawBody, err := io.ReadAll(req.Body) + assert.NoError(t, err) + assert.Equal(t, expectedRequestBody, string(rawBody)) + + rw.Header().Set("Content-Type", "application/json") + _, err = rw.Write([]byte(TraitResponseJson)) + assert.NoError(t, err) + })) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + err := client.CreateTrait(EnvironmentAPIKey, IdentityID, trait) + + // Then + assert.NoError(t, err) + assert.Equal(t, int64(1), trait.ID) +} + +func TestUpdateTrait(t *testing.T) { + // Given + trait_value := "updated_value" + trait := &flagsmithapi.Trait{ + ID: 1, + TraitKey: "key1", + ValueType: "unicode", + StringValue: &trait_value, + } + expectedRequestBody := `{"id":1,"trait_key":"key1","value_type":"unicode","string_value":"updated_value"}` + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, fmt.Sprintf("/api/v1/environments/%s/identities/%d/traits/1/", EnvironmentAPIKey, IdentityID), req.URL.Path) + assert.Equal(t, "PUT", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + + // Test that we sent the correct body + rawBody, err := io.ReadAll(req.Body) + assert.NoError(t, err) + assert.Equal(t, expectedRequestBody, string(rawBody)) + + rw.Header().Set("Content-Type", "application/json") + _, err = rw.Write([]byte(TraitResponseJson)) + assert.NoError(t, err) + })) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + err := client.UpdateTrait(EnvironmentAPIKey, IdentityID, trait) + + // Then + assert.NoError(t, err) + assert.Equal(t, "updated_value", *trait.StringValue) +} + +func TestDeleteTrait(t *testing.T) { + // Given + requestReceived := struct { + mu sync.Mutex + isRequestReceived bool + }{} + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + requestReceived.mu.Lock() + requestReceived.isRequestReceived = true + requestReceived.mu.Unlock() + + assert.Equal(t, fmt.Sprintf("/api/v1/environments/%s/identities/%d/traits/1/", EnvironmentAPIKey, IdentityID), req.URL.Path) + assert.Equal(t, "DELETE", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + })) + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + err := client.DeleteTrait(EnvironmentAPIKey, IdentityID, 1) + + // Then + requestReceived.mu.Lock() + assert.True(t, requestReceived.isRequestReceived) + requestReceived.mu.Unlock() + assert.NoError(t, err) +} diff --git a/models.go b/models.go index 3cb3ceb..0922bc3 100644 --- a/models.go +++ b/models.go @@ -3,13 +3,18 @@ package flagsmithapi import ( "encoding/json" "log" + "time" ) type Project struct { - ID int64 `json:"id"` - UUID string `json:"uuid"` - Name string `json:"name"` - Organisation int64 `json:"organisation"` + ID int64 `json:"id,omitempty"` + UUID string `json:"uuid,omitempty"` + Name string `json:"name"` + Organisation int64 `json:"organisation"` + HideDisabledFlags bool `json:"hide_disabled_flags,omitempty"` + PreventFlagDefaults bool `json:"prevent_flag_defaults,omitempty"` + OnlyAllowLowerCaseFeatureNames bool `json:"only_allow_lower_case_feature_names,omitempty"` + FeatureNameRegex bool `json:"feature_name_regex,omitempty"` } type FeatureMultivariateOption struct { @@ -206,11 +211,17 @@ type FeatureSegment struct { } type Environment struct { - ID int64 `json:"id,omitempty"` - Name string `json:"name"` - APIKey string `json:"api_key"` - Description string `json:"description"` - Project int64 `json:"project"` + ID int64 `json:"id,omitempty"` + Name string `json:"name"` + APIKey string `json:"api_key,omitempty"` + Description string `json:"description"` + Project int64 `json:"project"` + AllowClientTraits bool `json:"allow_client_traits,omitempty"` + BannerText string `json:"banner_text,omitempty"` + BannerColour string `json:"banner_colour,omitempty"` + HideDisabledFlags bool `json:"hide_disabled_flags,omitempty"` + HideSensitiveData bool `json:"hide_sensitive_data,omitempty"` + UseIdentityCompositeKeyForHashing bool `json:"use_identity_composite_key_for_hashing,omitempty"` } type Tag struct { @@ -223,3 +234,26 @@ type Tag struct { ProjectUUID string `json:"-"` ProjectID *int64 `json:"project,omitempty"` } + +type Identity struct { + ID *int64 `json:"id,omitempty"` + Identifier string `json:"identifier"` +} + +type ServerSideEnvKey struct { + ID int64 `json:"id,omitempty"` + Active bool `json:"active"` + Name string `json:"name,omitempty"` + Key string `json:"key,omitempty"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` +} +type Trait struct { + ID int64 `json:"id,omitempty"` + TraitKey string `json:"trait_key"` + ValueType string `json:"value_type"` + IntegerValue *int `json:"integer_value,omitempty"` + StringValue *string `json:"string_value,omitempty"` + BooleanValue *bool `json:"boolean_value,omitempty"` + FloatValue *float64 `json:"float_value,omitempty"` +} diff --git a/project.go b/project.go new file mode 100644 index 0000000..3ccd4e3 --- /dev/null +++ b/project.go @@ -0,0 +1,83 @@ +package flagsmithapi + +import ( + "fmt" +) + +func (c *Client) GetProject(projectUUID string) (*Project, error) { + url := fmt.Sprintf("%s/projects/get-by-uuid/%s/", c.baseURL, projectUUID) + project := Project{} + resp, err := c.client.R(). + SetResult(&project). + Get(url) + + if err != nil { + return nil, err + } + if !resp.IsSuccess() { + return nil, fmt.Errorf("flagsmithapi: Error getting project: %s", resp) + } + return &project, nil + +} +func (c *Client) GetProjectByID(projectID int64) (*Project, error) { + url := fmt.Sprintf("%s/projects/%d/", c.baseURL, projectID) + project := Project{} + resp, err := c.client.R(). + SetResult(&project). + Get(url) + + if err != nil { + return nil, err + } + if !resp.IsSuccess() { + return nil, fmt.Errorf("flagsmithapi: Error getting project: %s", resp) + } + return &project, nil + +} + +func (c *Client) CreateProject(project *Project) error { + url := fmt.Sprintf("%s/projects/", c.baseURL) + resp, err := c.client.R().SetBody(project).SetResult(project).Post(url) + + if err != nil { + return err + } + + if !resp.IsSuccess() { + return fmt.Errorf("flagsmithapi: Error creating project: %s", resp) + } + + return nil +} + +func (c *Client) UpdateProject(project *Project) error { + url := fmt.Sprintf("%s/projects/%d/", c.baseURL, project.ID) + resp, err := c.client.R().SetBody(project).SetResult(project).Put(url) + + if err != nil { + return err + } + + if !resp.IsSuccess() { + return fmt.Errorf("flagsmithapi: Error updating project: %s", resp) + } + + return nil +} + +func (c *Client) DeleteProject(projectID int64) error { + url := fmt.Sprintf("%s/projects/%d/", c.baseURL, projectID) + + resp, err := c.client.R().Delete(url) + if err != nil { + return err + } + + if !resp.IsSuccess() { + return fmt.Errorf("flagsmithapi: Error deleting project: %s", resp) + } + + return nil +} diff --git a/project_test.go b/project_test.go new file mode 100644 index 0000000..16f7ec7 --- /dev/null +++ b/project_test.go @@ -0,0 +1,175 @@ +package flagsmithapi_test + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "sync" + + "testing" + + "github.com/stretchr/testify/assert" + + flagsmithapi "github.com/Flagsmith/flagsmith-go-api-client" +) + +func TestGetProject(t *testing.T) { + // Given + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, fmt.Sprintf("/api/v1/projects/get-by-uuid/%s/", ProjectUUID), req.URL.Path) + assert.Equal(t, "GET", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + + rw.Header().Set("Content-Type", "application/json") + _, err := io.WriteString(rw, GetProjectResponseJson) + assert.NoError(t, err) + })) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + project, err := client.GetProject(ProjectUUID) + + // Then + assert.NoError(t, err) + + assert.Equal(t, ProjectID, project.ID) + assert.Equal(t, ProjectUUID, project.UUID) + assert.Equal(t, "project-1", project.Name) + +} + +func TestCreateProject(t *testing.T) { + // Given + project := flagsmithapi.Project{ + Name: ProjectName, + Organisation: OrganisationID, + } + expectedRequestBody := fmt.Sprintf(`{"name":"%s","organisation":%d}`, ProjectName, OrganisationID) + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, "/api/v1/projects/", req.URL.Path) + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + + // Test that we sent the correct body + rawBody, err := io.ReadAll(req.Body) + assert.NoError(t, err) + assert.Equal(t, expectedRequestBody, string(rawBody)) + + rw.Header().Set("Content-Type", "application/json") + _, err = io.WriteString(rw, GetProjectResponseJson) + assert.NoError(t, err) + + })) + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + err := client.CreateProject(&project) + + // Then + // assert that we did not receive an error + assert.NoError(t, err) + + assert.Equal(t, ProjectID, project.ID) + assert.Equal(t, ProjectUUID, project.UUID) + assert.Equal(t, "project-1", project.Name) + +} +func TestUpdateProject(t *testing.T) { + // Given + project := flagsmithapi.Project{ + ID: ProjectID, + Name: ProjectName, + Organisation: OrganisationID, + } + expectedRequestBody := fmt.Sprintf(`{"id":%d,"name":"%s","organisation":%d}`, ProjectID, ProjectName, OrganisationID) + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, fmt.Sprintf("/api/v1/projects/%d/", ProjectID), req.URL.Path) + assert.Equal(t, "PUT", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + + // Test that we sent the correct body + rawBody, err := io.ReadAll(req.Body) + assert.NoError(t, err) + assert.Equal(t, expectedRequestBody, string(rawBody)) + + rw.Header().Set("Content-Type", "application/json") + _, err = io.WriteString(rw, GetProjectResponseJson) + assert.NoError(t, err) + + })) + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + err := client.UpdateProject(&project) + + // Then + // assert that we did not receive an error + assert.NoError(t, err) + + assert.Equal(t, ProjectID, project.ID) + assert.Equal(t, ProjectUUID, project.UUID) + assert.Equal(t, "project-1", project.Name) + +} +func TestGetProjectByID(t *testing.T) { + // Given + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, fmt.Sprintf("/api/v1/projects/%d/", ProjectID), req.URL.Path) + assert.Equal(t, "GET", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + + rw.Header().Set("Content-Type", "application/json") + _, err := io.WriteString(rw, GetProjectResponseJson) + assert.NoError(t, err) + })) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + project, err := client.GetProjectByID(ProjectID) + + // Then + assert.NoError(t, err) + + assert.Equal(t, ProjectID, project.ID) + assert.Equal(t, ProjectUUID, project.UUID) + assert.Equal(t, "project-1", project.Name) + +} +func TestDeleteProject(t *testing.T) { + // Given + requestReceived := struct { + mu sync.Mutex + isRequestReceived bool + }{} + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + requestReceived.mu.Lock() + requestReceived.isRequestReceived = true + requestReceived.mu.Unlock() + + assert.Equal(t, fmt.Sprintf("/api/v1/projects/%d/", ProjectID), req.URL.Path) + assert.Equal(t, "DELETE", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + + })) + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + err := client.DeleteProject(ProjectID) + + // Then + requestReceived.mu.Lock() + assert.True(t, requestReceived.isRequestReceived) + assert.NoError(t, err) + +}