From b617e7591b305f6c48621da06e1f9ff579b3ac28 Mon Sep 17 00:00:00 2001 From: Matthew Elwell Date: Tue, 25 Aug 2026 17:39:03 +0100 Subject: [PATCH] feat: Add GetFeatureByName and GetSegmentByName Adds lookups by name for features and segments, so that callers which know a project UUID and a name -- but not a UUID -- can resolve an entity. This is what the Terraform provider needs to offer `flagsmith_feature` and `flagsmith_segment` data sources. Two things about the API are worth knowing: - Features filter on `search`, segments filter on `q`. Passing the wrong one is silently ignored and returns every result in the project. - Both filters are case insensitive "contains" matches, so results have to be filtered client side for an exact match. Without that, asking for `flag` could return `flagship`. Each lookup is a single request. CustomPagination allows at most 999 results per page, which is sent explicitly so that a change to the API's default page size cannot silently truncate a search. A truncated page means an exact match may have been missed, so that returns an error rather than a false "not found"; it takes a project with more than 999 entities whose names all contain the search term to reach. Segment names are not unique within a project, so a name that matches more than one segment returns MultipleSegmentsFoundError rather than picking arbitrarily. Feature names are uniquely indexed, so the equivalent feature error is defensive only. Co-Authored-By: Claude Opus 5 --- client.go | 133 ++++++++++++++ errors.go | 36 ++++ search_test.go | 470 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 639 insertions(+) create mode 100644 search_test.go diff --git a/client.go b/client.go index a6c4b04..9745296 100644 --- a/client.go +++ b/client.go @@ -135,6 +135,68 @@ func (c *Client) GetFeature(featureUUID string) (*Feature, error) { return &feature, nil } +// GetFeatureByName returns the feature in the given project whose name is an exact +// match for featureName. +// +// The match is case sensitive. The API's `search` filter is a case insensitive +// "contains" match, so results are filtered client side. +func (c *Client) GetFeatureByName(projectUUID string, featureName string) (*Feature, error) { + projectID, err := c.getProjectID(projectUUID) + if err != nil { + return nil, err + } + + url := fmt.Sprintf("%s/projects/%d/features/", c.baseURL, projectID) + result := struct { + Next *string `json:"next"` + Results []*Feature `json:"results"` + }{} + + resp, err := c.client.R(). + SetQueryParams(map[string]string{ + "search": featureName, + "page_size": searchPageSize, + }). + SetResult(&result). + Get(url) + + if err != nil { + return nil, err + } + + if !resp.IsSuccess() { + return nil, fmt.Errorf("flagsmithapi: Error searching features: %s", resp) + } + + // `search` is a "contains" match, so a full page means results may have been + // truncated and an exact match could have been missed. This needs a project with + // more than searchPageSize features whose names all contain featureName. + if result.Next != nil { + return nil, fmt.Errorf("flagsmithapi: too many features in project '%s' have names containing '%s' to identify an exact match", + projectUUID, featureName) + } + + var matches []*Feature + for _, feature := range result.Results { + if feature.Name == featureName { + matches = append(matches, feature) + } + } + + switch len(matches) { + case 0: + return nil, FeatureNotFoundError{featureName: featureName, projectUUID: projectUUID} + case 1: + feature := matches[0] + // The API never returns the project UUID, and unlike GetFeature we already + // know it, so there is no need to look the project up again. + feature.ProjectUUID = projectUUID + return feature, nil + default: + return nil, MultipleFeaturesFoundError{featureName: featureName, projectUUID: projectUUID, count: len(matches)} + } +} + func (c *Client) CreateFeature(feature *Feature) error { if feature.ProjectID == nil { projectID, err := c.getProjectID(feature.ProjectUUID) @@ -189,6 +251,11 @@ func (c *Client) UpdateFeature(feature *Feature) error { return nil } +// searchPageSize is CustomPagination.max_page_size. It is also the API's default, but +// is sent explicitly so that a change to that default cannot silently start truncating +// search results. +const searchPageSize = "999" + func (c *Client) getProjectID(projectUUID string) (int64, error) { project, err := c.GetProject(projectUUID) @@ -376,6 +443,72 @@ func (c *Client) GetSegment(segmentUUID string) (*Segment, error) { segment.ProjectUUID = project.UUID return &segment, nil } + +// GetSegmentByName returns the segment in the given project whose name is an exact +// match for segmentName. +// +// The match is case sensitive. The API's `q` filter is a case insensitive +// "contains" match, so results are filtered client side. +// +// Note that segment names are not unique within a project: if more than one segment +// matches, a MultipleSegmentsFoundError is returned. System segments are excluded by +// the list endpoint, so they can only be looked up by UUID. +func (c *Client) GetSegmentByName(projectUUID string, segmentName string) (*Segment, error) { + projectID, err := c.getProjectID(projectUUID) + if err != nil { + return nil, err + } + + url := fmt.Sprintf("%s/projects/%d/segments/", c.baseURL, projectID) + result := struct { + Next *string `json:"next"` + Results []*Segment `json:"results"` + }{} + + resp, err := c.client.R(). + SetQueryParams(map[string]string{ + // NOTE: segments filter on `q`, features filter on `search`. Passing + // `search` here is silently ignored and returns every segment. + "q": segmentName, + "page_size": searchPageSize, + }). + SetResult(&result). + Get(url) + + if err != nil { + return nil, err + } + + if !resp.IsSuccess() { + return nil, fmt.Errorf("flagsmithapi: Error searching segments: %s", resp) + } + + if result.Next != nil { + return nil, fmt.Errorf("flagsmithapi: too many segments in project '%s' have names containing '%s' to identify an exact match", + projectUUID, segmentName) + } + + var matches []*Segment + for _, segment := range result.Results { + if segment.Name == segmentName { + matches = append(matches, segment) + } + } + + switch len(matches) { + case 0: + return nil, SegmentNotFoundError{segmentName: segmentName, projectUUID: projectUUID} + case 1: + segment := matches[0] + // The API never returns the project UUID, and unlike GetSegment we already + // know it, so there is no need to look the project up again. + segment.ProjectUUID = projectUUID + return segment, nil + default: + return nil, MultipleSegmentsFoundError{segmentName: segmentName, projectUUID: projectUUID, count: len(matches)} + } +} + func (c *Client) DeleteSegment(projectID, segmentID int64) error { url := fmt.Sprintf("%s/projects/%d/segments/%d/", c.baseURL, projectID, segmentID) diff --git a/errors.go b/errors.go index c8348f9..c9d21c9 100644 --- a/errors.go +++ b/errors.go @@ -6,12 +6,16 @@ import ( type FeatureNotFoundError struct { featureUUID string + featureName string + projectUUID string } type FeatureStateNotFoundError struct { featureStateUUID string } type SegmentNotFoundError struct { segmentUUID string + segmentName string + projectUUID string } type FeatureMVOptionNotFoundError struct { featureMVOptionUUID string @@ -20,14 +24,46 @@ type UserNotFoundError struct { email string } +// MultipleFeaturesFoundError is returned when a lookup by name matches more than +// one feature. Feature names are unique within a project, so this is defensive. +type MultipleFeaturesFoundError struct { + featureName string + projectUUID string + count int +} + +// MultipleSegmentsFoundError is returned when a lookup by name matches more than +// one segment. Unlike features, segment names are not unique within a project. +type MultipleSegmentsFoundError struct { + segmentName string + projectUUID string + count int +} + func (e FeatureNotFoundError) Error() string { + if e.featureName != "" { + return fmt.Sprintf("flagsmithapi: feature named '%s' not found in project '%s'", e.featureName, e.projectUUID) + } return fmt.Sprintf("flagsmithapi: feature '%s' not found", e.featureUUID) } func (e SegmentNotFoundError) Error() string { + if e.segmentName != "" { + return fmt.Sprintf("flagsmithapi: segment named '%s' not found in project '%s'", e.segmentName, e.projectUUID) + } return fmt.Sprintf("flagsmithapi: segment '%s' not found", e.segmentUUID) } +func (e MultipleFeaturesFoundError) Error() string { + return fmt.Sprintf("flagsmithapi: found %d features named '%s' in project '%s', expected exactly one", + e.count, e.featureName, e.projectUUID) +} + +func (e MultipleSegmentsFoundError) Error() string { + return fmt.Sprintf("flagsmithapi: found %d segments named '%s' in project '%s', expected exactly one", + e.count, e.segmentName, e.projectUUID) +} + func (e FeatureStateNotFoundError) Error() string { return fmt.Sprintf("flagsmithapi: feature state '%s' not found", e.featureStateUUID) } diff --git a/search_test.go b/search_test.go new file mode 100644 index 0000000..d7ddf5a --- /dev/null +++ b/search_test.go @@ -0,0 +1,470 @@ +package flagsmithapi_test + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + + flagsmithapi "github.com/Flagsmith/flagsmith-go-api-client" +) + +// searchResultsJson wraps result objects in the paginated envelope the API returns. +// next is emitted verbatim, so pass "null" for the last page. +func searchResultsJson(next string, results ...string) string { + body := "" + for i, result := range results { + if i > 0 { + body += "," + } + body += result + } + return fmt.Sprintf(`{"count": %d, "next": %s, "previous": null, "results": [%s]}`, + len(results), next, body) +} + +func featureJson(id int64, name string) string { + return fmt.Sprintf(`{ + "id": %d, + "uuid": "%s", + "name": "%s", + "project": %d, + "type": "STANDARD", + "default_enabled": false, + "initial_value": null, + "description": null, + "tags": [1], + "is_archived": false, + "owners": [{"id": 1, "email": "some_user@email.com"}], + "group_owners": [{"id": 3, "name": "Test Group"}] + }`, id, FeatureUUID, name, ProjectID) +} + +func segmentJson(id int64, name string, feature string) string { + return fmt.Sprintf(`{ + "id": %d, + "uuid": "%s", + "name": "%s", + "description": null, + "project": %d, + "feature": %s, + "rules": [ + { + "type": "ALL", + "rules": [ + { + "type": "ANY", + "rules": [], + "conditions": [{"operator": "EQUAL", "property": "device_type", "value": "mobile"}] + } + ], + "conditions": [] + } + ] + }`, id, SegmentUUID, name, ProjectID, feature) +} + +// searchTestServer serves the project lookup and a project sub-resource list endpoint. +// It records the query parameters of every list request, and fails the test if the +// project-by-ID endpoint (which name lookups should never need) is called. +func searchTestServer(t *testing.T, subResource string, pages ...string) (*httptest.Server, *[]url.Values) { + t.Helper() + + mux := http.NewServeMux() + var mu sync.Mutex + queries := []url.Values{} + + mux.HandleFunc(fmt.Sprintf("/api/v1/projects/get-by-uuid/%s/", ProjectUUID), func(rw http.ResponseWriter, req *http.Request) { + 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) + }) + + mux.HandleFunc(fmt.Sprintf("/api/v1/projects/%d/", ProjectID), func(rw http.ResponseWriter, req *http.Request) { + assert.Fail(t, "the project should not be looked up by ID during a lookup by name") + }) + + mux.HandleFunc(fmt.Sprintf("/api/v1/projects/%d/%s/", ProjectID, subResource), func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, "GET", req.Method) + assert.Equal(t, "Api-Key "+MasterAPIKey, req.Header.Get("Authorization")) + + mu.Lock() + page := len(queries) + queries = append(queries, req.URL.Query()) + mu.Unlock() + + if page >= len(pages) { + assert.Failf(t, "unexpected request", "requested page %d but only %d were stubbed", page+1, len(pages)) + return + } + + rw.Header().Set("Content-Type", "application/json") + _, err := io.WriteString(rw, pages[page]) + assert.NoError(t, err) + }) + + return httptest.NewServer(mux), &queries +} + +func TestGetFeatureByName(t *testing.T) { + // Given + server, queries := searchTestServer(t, "features", + searchResultsJson("null", featureJson(FeatureID, FeatureName))) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + feature, err := client.GetFeatureByName(ProjectUUID, FeatureName) + + // Then + assert.NoError(t, err) + + assert.Equal(t, FeatureID, *feature.ID) + assert.Equal(t, FeatureName, feature.Name) + assert.Equal(t, FeatureUUID, feature.UUID) + assert.Equal(t, "STANDARD", *feature.Type) + assert.Equal(t, false, feature.DefaultEnabled) + assert.Equal(t, false, feature.IsArchived) + assert.Equal(t, []int64{1}, feature.Tags) + assert.Equal(t, &[]int64{1}, feature.Owners) + assert.Equal(t, &[]int64{3}, feature.GroupOwners) + + assert.Equal(t, ProjectID, *feature.ProjectID) + // The API never returns the project UUID, so the client must set it from the + // argument, otherwise Terraform sees an inconsistent data source result. + assert.Equal(t, ProjectUUID, feature.ProjectUUID) + + // Features filter on `search`. + assert.Equal(t, 1, len(*queries)) + assert.Equal(t, FeatureName, (*queries)[0].Get("search")) + // CustomPagination.max_page_size, sent so a change to the API's default page size + // cannot silently truncate results. + assert.Equal(t, "999", (*queries)[0].Get("page_size")) +} + +func TestGetFeatureByNameFiltersPartialMatches(t *testing.T) { + // Given + // The API's `search` filter is a "contains" match, so it returns both of these. + server, _ := searchTestServer(t, "features", searchResultsJson("null", + featureJson(2, FeatureName+"_suffix"), + featureJson(FeatureID, FeatureName), + )) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + feature, err := client.GetFeatureByName(ProjectUUID, FeatureName) + + // Then + assert.NoError(t, err) + assert.Equal(t, FeatureName, feature.Name) + assert.Equal(t, FeatureID, *feature.ID) +} + +func TestGetFeatureByNameOnlyPartialMatches(t *testing.T) { + // Given + server, _ := searchTestServer(t, "features", searchResultsJson("null", + featureJson(2, FeatureName+"_suffix"), + )) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + feature, err := client.GetFeatureByName(ProjectUUID, FeatureName) + + // Then + assert.Nil(t, feature) + assert.IsType(t, flagsmithapi.FeatureNotFoundError{}, err) + assert.Contains(t, err.Error(), FeatureName) + assert.Contains(t, err.Error(), ProjectUUID) +} + +func TestGetFeatureByNameIsCaseSensitive(t *testing.T) { + // Given + server, _ := searchTestServer(t, "features", searchResultsJson("null", + featureJson(FeatureID, "Test_Feature"), + )) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + feature, err := client.GetFeatureByName(ProjectUUID, "test_feature") + + // Then + assert.Nil(t, feature) + assert.IsType(t, flagsmithapi.FeatureNotFoundError{}, err) +} + +func TestGetFeatureByNameNotFound(t *testing.T) { + // Given + server, _ := searchTestServer(t, "features", searchResultsJson("null")) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + feature, err := client.GetFeatureByName(ProjectUUID, FeatureName) + + // Then + assert.Nil(t, feature) + assert.IsType(t, flagsmithapi.FeatureNotFoundError{}, err) +} + +// `search` is a "contains" match, so a truncated page means an exact match may have been +// missed. Erroring is better than a false "not found". +func TestGetFeatureByNameErrorsWhenResultsAreTruncated(t *testing.T) { + // Given + server, queries := searchTestServer(t, "features", + searchResultsJson(`"http://api.example.com/api/v1/projects/10/features/?page=2"`, + featureJson(2, FeatureName+"_suffix")), + ) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + feature, err := client.GetFeatureByName(ProjectUUID, FeatureName) + + // Then + assert.Nil(t, feature) + assert.Error(t, err) + assert.Contains(t, err.Error(), "too many features") + // A truncated search must not be reported as "not found". + _, isNotFound := err.(flagsmithapi.FeatureNotFoundError) + assert.False(t, isNotFound) + + // Only one request is made; the results are not walked page by page. + assert.Equal(t, 1, len(*queries)) +} + +func TestGetFeatureByNameMultipleMatches(t *testing.T) { + // Given + server, _ := searchTestServer(t, "features", searchResultsJson("null", + featureJson(FeatureID, FeatureName), + featureJson(2, FeatureName), + )) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + feature, err := client.GetFeatureByName(ProjectUUID, FeatureName) + + // Then + assert.Nil(t, feature) + assert.IsType(t, flagsmithapi.MultipleFeaturesFoundError{}, err) + assert.Contains(t, err.Error(), "found 2 features") +} + +func TestGetFeatureByNameAPIError(t *testing.T) { + // Given + mux := http.NewServeMux() + + mux.HandleFunc(fmt.Sprintf("/api/v1/projects/get-by-uuid/%s/", ProjectUUID), func(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set("Content-Type", "application/json") + _, err := io.WriteString(rw, GetProjectResponseJson) + assert.NoError(t, err) + }) + + mux.HandleFunc(fmt.Sprintf("/api/v1/projects/%d/features/", ProjectID), func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusInternalServerError) + }) + + server := httptest.NewServer(mux) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + feature, err := client.GetFeatureByName(ProjectUUID, FeatureName) + + // Then + assert.Nil(t, feature) + assert.Error(t, err) + // A server error must not be reported as "not found". + _, isNotFound := err.(flagsmithapi.FeatureNotFoundError) + assert.False(t, isNotFound) + assert.Contains(t, err.Error(), "Error searching features") +} + +func TestGetFeatureByNameProjectNotFound(t *testing.T) { + // Given + mux := http.NewServeMux() + + mux.HandleFunc(fmt.Sprintf("/api/v1/projects/get-by-uuid/%s/", ProjectUUID), func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusNotFound) + }) + + mux.HandleFunc(fmt.Sprintf("/api/v1/projects/%d/features/", ProjectID), func(rw http.ResponseWriter, req *http.Request) { + assert.Fail(t, "features should not be searched when the project cannot be resolved") + }) + + server := httptest.NewServer(mux) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + feature, err := client.GetFeatureByName(ProjectUUID, FeatureName) + + // Then + assert.Nil(t, feature) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Error getting project") +} + +func TestGetSegmentByName(t *testing.T) { + // Given + segmentName := "mobile_users" + server, queries := searchTestServer(t, "segments", + searchResultsJson("null", segmentJson(SegmentID, segmentName, "null"))) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + segment, err := client.GetSegmentByName(ProjectUUID, segmentName) + + // Then + var nilIntPointer *int64 + var nilStringPointer *string + + assert.NoError(t, err) + + assert.Equal(t, SegmentID, *segment.ID) + assert.Equal(t, SegmentUUID, segment.UUID) + assert.Equal(t, segmentName, segment.Name) + assert.Equal(t, nilStringPointer, segment.Description) + assert.Equal(t, nilIntPointer, segment.FeatureID) + + assert.Equal(t, ProjectID, *segment.ProjectID) + assert.Equal(t, ProjectUUID, segment.ProjectUUID) + + // The list endpoint returns the full rules tree, so it round trips exactly as + // the get-by-uuid response does. + assert.Equal(t, 1, len(segment.Rules)) + assert.Equal(t, "ALL", segment.Rules[0].Type) + assert.Equal(t, 0, len(segment.Rules[0].Conditions)) + assert.Equal(t, 1, len(segment.Rules[0].Rules)) + assert.Equal(t, "ANY", segment.Rules[0].Rules[0].Type) + assert.Equal(t, 1, len(segment.Rules[0].Rules[0].Conditions)) + assert.Equal(t, "EQUAL", segment.Rules[0].Rules[0].Conditions[0].Operator) + assert.Equal(t, "device_type", segment.Rules[0].Rules[0].Conditions[0].Property) + assert.Equal(t, "mobile", segment.Rules[0].Rules[0].Conditions[0].Value) + + // Segments filter on `q`, not `search`. + assert.Equal(t, 1, len(*queries)) + assert.Equal(t, segmentName, (*queries)[0].Get("q")) + assert.Equal(t, "", (*queries)[0].Get("search")) + assert.Equal(t, "999", (*queries)[0].Get("page_size")) +} + +func TestGetSegmentByNameFiltersPartialMatches(t *testing.T) { + // Given + segmentName := "mobile_users" + server, _ := searchTestServer(t, "segments", searchResultsJson("null", + segmentJson(2, segmentName+"_eu", "null"), + segmentJson(SegmentID, segmentName, "null"), + )) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + segment, err := client.GetSegmentByName(ProjectUUID, segmentName) + + // Then + assert.NoError(t, err) + assert.Equal(t, SegmentID, *segment.ID) + assert.Equal(t, segmentName, segment.Name) +} + +func TestGetSegmentByNameFeatureSpecific(t *testing.T) { + // Given + segmentName := "feature_specific_segment" + server, _ := searchTestServer(t, "segments", searchResultsJson("null", + segmentJson(SegmentID, segmentName, "25142"), + )) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + segment, err := client.GetSegmentByName(ProjectUUID, segmentName) + + // Then + assert.NoError(t, err) + assert.Equal(t, int64(25142), *segment.FeatureID) +} + +func TestGetSegmentByNameNotFound(t *testing.T) { + // Given + server, _ := searchTestServer(t, "segments", searchResultsJson("null")) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + segment, err := client.GetSegmentByName(ProjectUUID, "missing_segment") + + // Then + assert.Nil(t, segment) + assert.IsType(t, flagsmithapi.SegmentNotFoundError{}, err) + assert.Contains(t, err.Error(), "missing_segment") + assert.Contains(t, err.Error(), ProjectUUID) +} + +func TestGetSegmentByNameErrorsWhenResultsAreTruncated(t *testing.T) { + // Given + segmentName := "mobile_users" + server, queries := searchTestServer(t, "segments", + searchResultsJson(`"http://api.example.com/api/v1/projects/10/segments/?page=2"`, + segmentJson(2, segmentName+"_eu", "null")), + ) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + segment, err := client.GetSegmentByName(ProjectUUID, segmentName) + + // Then + assert.Nil(t, segment) + assert.Error(t, err) + assert.Contains(t, err.Error(), "too many segments") + + assert.Equal(t, 1, len(*queries)) +} + +func TestGetSegmentByNameMultipleMatches(t *testing.T) { + // Given + // Segment names are not unique within a project. + segmentName := "mobile_users" + server, _ := searchTestServer(t, "segments", searchResultsJson("null", + segmentJson(SegmentID, segmentName, "null"), + segmentJson(2, segmentName, "null"), + )) + defer server.Close() + + client := flagsmithapi.NewClient(MasterAPIKey, server.URL+"/api/v1") + + // When + segment, err := client.GetSegmentByName(ProjectUUID, segmentName) + + // Then + assert.Nil(t, segment) + assert.IsType(t, flagsmithapi.MultipleSegmentsFoundError{}, err) + assert.Contains(t, err.Error(), "found 2 segments") +}