Skip to content

Commit

Permalink
More evaluation tests / consistent user attribute conversions (#71)
Browse files Browse the repository at this point in the history
* Add more evaluation tests / align user attribute conversions

* Bump version

* Replace codecov with sonar

* Update go-ci.yml

* Fixup evaluation error handling

* Specify error types

* More tests
  • Loading branch information
z4kn4fein authored Apr 11, 2024
1 parent 08a74b9 commit 6467281
Show file tree
Hide file tree
Showing 24 changed files with 3,246 additions and 150 deletions.
13 changes: 9 additions & 4 deletions .github/workflows/go-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,18 @@ jobs:
needs: test
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.x

- name: Run Coverage
run: go test -coverprofile=coverage.txt -covermode=atomic

- name: Upload Report
run: bash <(curl -s https://codecov.io/bash)
run: go test -coverprofile=coverage.out -covermode=atomic ./...

- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ ConfigCat is a <a target="_blank" href="https://configcat.com">hosted feature fl

[![Build Status](https://github.com/configcat/go-sdk/actions/workflows/go-ci.yml/badge.svg?branch=v9)](https://github.com/configcat/go-sdk/actions/workflows/go-ci.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/configcat/go-sdk/v9)](https://goreportcard.com/report/github.com/configcat/go-sdk/v9)
[![codecov](https://codecov.io/gh/configcat/go-sdk/branch/v9/graph/badge.svg)](https://codecov.io/gh/configcat/go-sdk)
[![GoDoc](https://godoc.org/github.com/configcat/go-sdk?status.svg)](https://pkg.go.dev/github.com/configcat/go-sdk/v9)
![License](https://img.shields.io/github/license/configcat/go-sdk.svg)
[![Sonar Coverage](https://img.shields.io/sonar/coverage/configcat_go-sdk?logo=SonarCloud&server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/project/overview?id=configcat_go-sdk)
[![Sonar Quality Gate](https://img.shields.io/sonar/quality_gate/configcat_go-sdk?logo=sonarcloud&server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/project/overview?id=configcat_go-sdk)

## Getting started

Expand Down
4 changes: 2 additions & 2 deletions config_fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ func (f *configFetcher) fetchHTTPWithoutRedirect(ctx context.Context, baseURL st
if os.IsTimeout(err) {
return nil, &fetcherError{EventId: 1102, Err: fmt.Errorf("request timed out while trying to fetch config JSON. (timeout value: %dms) %v", f.timeout.Milliseconds(), err)}
} else {
return nil, &fetcherError{EventId: 1103, Err: fmt.Errorf("unexpected error occurred while trying to fetch config JSON: %v", err)}
return nil, &fetcherError{EventId: 1103, Err: fmt.Errorf("unexpected error occurred while trying to fetch config JSON; it is most likely due to a local network issue; please make sure your application can reach the ConfigCat CDN servers (or your proxy server) over HTTP: %v", err)}
}
}
defer response.Body.Close()
Expand All @@ -407,7 +407,7 @@ func (f *configFetcher) fetchHTTPWithoutRedirect(ctx context.Context, baseURL st
if response.StatusCode >= 200 && response.StatusCode < 300 {
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, &fetcherError{EventId: 1103, Err: fmt.Errorf("unexpected error occurred while trying to fetch config JSON; read failed: %v", err)}
return nil, &fetcherError{EventId: 1103, Err: fmt.Errorf("unexpected error occurred while trying to fetch config JSON; it is most likely due to a local network issue; please make sure your application can reach the ConfigCat CDN servers (or your proxy server) over HTTP: %v", err)}
}
config, err := parseConfig(body, response.Header.Get("Etag"), time.Now(), f.logger, f.defaultUser, f.overrides, f.hooks)
if err != nil {
Expand Down
80 changes: 78 additions & 2 deletions configcat_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,18 @@ func TestClient_GetWithInvalidConfig(t *testing.T) {
c.Assert(result, qt.Equals, "default")
}

func TestClient_GetDetails_WithInvalidConfig(t *testing.T) {
c := qt.New(t)
srv, client := getTestClients(t)
srv.setResponse(configResponse{body: "invalid-json"})
client.Refresh(context.Background())
result := client.GetStringValueDetails("key", "default", nil)
_, ok := result.Data.Error.(ErrConfigJsonMissing)
c.Assert(ok, qt.IsTrue)
c.Assert(result.Data.IsDefaultValue, qt.IsTrue)
c.Assert(result.Value, qt.Equals, "default")
}

func TestClient_GetInt(t *testing.T) {
c := qt.New(t)
srv, client := getTestClients(t)
Expand Down Expand Up @@ -751,6 +763,70 @@ func TestClient_GetFloatDetails_NotExist(t *testing.T) {
c.Assert(details.Value, qt.Equals, float64(0))
}

func TestClient_GetBoolDetails_TypeMismatch(t *testing.T) {
c := qt.New(t)
srv := newConfigServer(t)
srv.setResponse(configResponse{
body: contentForIntegrationTestKey("PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A"),
})
client := NewCustomClient(srv.config())
client.Refresh(context.Background())

details := client.GetBoolValueDetails("integerDefaultOne", false, nil)
err, ok := details.Data.Error.(ErrSettingTypeMismatch)
c.Assert(ok, qt.IsTrue)
c.Assert(details.Value, qt.IsFalse)
c.Assert(err.Error(), qt.Equals, "the type of the setting 'integerDefaultOne' doesn't match with the expected type; setting's type was 'int' but the expected type was 'bool'")
}

func TestClient_GetStringDetails_TypeMismatch(t *testing.T) {
c := qt.New(t)
srv := newConfigServer(t)
srv.setResponse(configResponse{
body: contentForIntegrationTestKey("PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A"),
})
client := NewCustomClient(srv.config())
client.Refresh(context.Background())

details := client.GetStringValueDetails("integerDefaultOne", "", nil)
err, ok := details.Data.Error.(ErrSettingTypeMismatch)
c.Assert(ok, qt.IsTrue)
c.Assert(details.Value, qt.Equals, "")
c.Assert(err.Error(), qt.Equals, "the type of the setting 'integerDefaultOne' doesn't match with the expected type; setting's type was 'int' but the expected type was 'string'")
}

func TestClient_GetIntDetails_TypeMismatch(t *testing.T) {
c := qt.New(t)
srv := newConfigServer(t)
srv.setResponse(configResponse{
body: contentForIntegrationTestKey("PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A"),
})
client := NewCustomClient(srv.config())
client.Refresh(context.Background())

details := client.GetIntValueDetails("boolDefaultTrue", 0, nil)
err, ok := details.Data.Error.(ErrSettingTypeMismatch)
c.Assert(ok, qt.IsTrue)
c.Assert(details.Value, qt.Equals, 0)
c.Assert(err.Error(), qt.Equals, "the type of the setting 'boolDefaultTrue' doesn't match with the expected type; setting's type was 'bool' but the expected type was 'int'")
}

func TestClient_GetFloatDetails_TypeMismatch(t *testing.T) {
c := qt.New(t)
srv := newConfigServer(t)
srv.setResponse(configResponse{
body: contentForIntegrationTestKey("PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A"),
})
client := NewCustomClient(srv.config())
client.Refresh(context.Background())

details := client.GetFloatValueDetails("boolDefaultTrue", 0, nil)
err, ok := details.Data.Error.(ErrSettingTypeMismatch)
c.Assert(ok, qt.IsTrue)
c.Assert(details.Value, qt.Equals, float64(0))
c.Assert(err.Error(), qt.Equals, "the type of the setting 'boolDefaultTrue' doesn't match with the expected type; setting's type was 'bool' but the expected type was 'float'")
}

func TestClient_GetDetails_Reflected_User(t *testing.T) {
c := qt.New(t)
srv := newConfigServer(t)
Expand Down Expand Up @@ -957,8 +1033,8 @@ func TestCacheKey(t *testing.T) {
key string
cacheKey string
}{
{"test1", "7f845c43ecc95e202b91e271435935e6d1391e5d"},
{"test2", "a78b7e323ef543a272c74540387566a22415148a"},
{"configcat-sdk-1/TEST_KEY-0123456789012/1234567890123456789012", "f83ba5d45bceb4bb704410f51b704fb6dfa19942"},
{"configcat-sdk-1/TEST_KEY2-123456789012/1234567890123456789012", "da7bfd8662209c8ed3f9db96daed4f8d91ba5876"},
}

l := newTestLogger(t)
Expand Down
31 changes: 27 additions & 4 deletions configcattest/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ type Flag struct {
// If any rule is satisfied, its associated value is used,
// otherwise the default value is used.
Rules []Rule

// Percentages holds a set of percentage options to evaluate.
Percentages []PercentageOption

// PercentageEvalAttribute is the User Object attribute which serves as the basis of percentage options evaluation.
PercentageEvalAttribute string
}

type Rule struct {
Expand All @@ -55,17 +61,24 @@ type Rule struct {
Value interface{}
}

type PercentageOption struct {
Percentage int64
Value interface{}
}

func (f *Flag) entry(key string) (*configcat.Setting, error) {
ft := typeOf(f.Default)
if ft == invalidType {
return nil, fmt.Errorf("invalid type %T for default value %#v", f.Default, f.Default)
}
e := &configcat.Setting{
VariationID: "v_" + key,
Type: ft,
Value: &configcat.SettingValue{Value: f.Default},
TargetingRules: make([]*configcat.TargetingRule, 0, len(f.Rules)),
PercentageOptionsAttribute: f.PercentageEvalAttribute,
VariationID: "v_" + key,
Type: ft,
Value: &configcat.SettingValue{Value: f.Default},
TargetingRules: make([]*configcat.TargetingRule, 0, len(f.Rules)),
}

for i, rule := range f.Rules {
if rule.Comparator.String() == "" {
return nil, fmt.Errorf("invalid comparator value %d", rule.Comparator)
Expand Down Expand Up @@ -103,5 +116,15 @@ func (f *Flag) entry(key string) (*configcat.Setting, error) {
},
})
}
if len(f.Percentages) > 0 {
for _, p := range f.Percentages {
e.TargetingRules = append(e.TargetingRules, &configcat.TargetingRule{
PercentageOptions: []*configcat.PercentageOption{{
Percentage: p.Percentage,
Value: &configcat.SettingValue{Value: p.Value},
}},
})
}
}
return e, nil
}
42 changes: 42 additions & 0 deletions configcattest/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,48 @@ func TestHandlerWithUser(t *testing.T) {
})), qt.Equals, 99)
}

func TestHandlerWithPercentage(t *testing.T) {
c := qt.New(t)
k := configcattest.RandomSDKKey()
var h configcattest.Handler
err := h.SetFlags(k, map[string]*configcattest.Flag{
"someflag": {
PercentageEvalAttribute: "foo",
Default: 99,
Percentages: []configcattest.PercentageOption{{
Percentage: 30,
Value: 30,
}, {
Percentage: 70,
Value: 70,
}},
},
})
c.Assert(err, qt.IsNil)
srv := httptest.NewServer(&h)
defer srv.Close()
client := configcat.NewCustomClient(configcat.Config{
BaseURL: srv.URL,
SDKKey: k,
})
defer client.Close()
flag := configcat.Int("someflag", 1)
c.Assert(flag.Get(client.Snapshot(nil)), qt.Equals, 99)

type user struct {
Foo string `configcat:"foo"`
}
c.Assert(flag.Get(client.Snapshot(&user{
Foo: "something",
})), qt.Equals, 70)
c.Assert(flag.Get(client.Snapshot(&user{
Foo: "dwvhyauiwdvyiu",
})), qt.Equals, 70)
c.Assert(flag.Get(client.Snapshot(&user{
Foo: "pjhiboub;ib",
})), qt.Equals, 30)
}

var invalidFlagsTests = []struct {
testName string
flag *configcattest.Flag
Expand Down
52 changes: 52 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package configcat

import (
"fmt"
"strings"
)

// ErrKeyNotFound is returned when a key is not found in the configuration.
type ErrKeyNotFound struct {
Key string
AvailableKeys []string
}

func (e ErrKeyNotFound) Error() string {
var availableKeys = ""
if len(e.AvailableKeys) > 0 {
availableKeys = "'" + strings.Join(e.AvailableKeys, "', '") + "'"
}
return fmt.Sprintf(
"failed to evaluate setting '%s' (the key was not found in config JSON); available keys: [%s]",
e.Key,
availableKeys,
)
}

// ErrSettingTypeMismatch is returned when a requested setting type doesn't match with the expected type.
type ErrSettingTypeMismatch struct {
Key string
Value interface{}
ExpectedType string
}

func (e ErrSettingTypeMismatch) Error() string {
return fmt.Sprintf(
"the type of the setting '%s' doesn't match with the expected type; setting's type was '%T' but the expected type was '%s'",
e.Key,
e.Value,
e.ExpectedType,
)
}

// ErrConfigJsonMissing is returned when the config JSON is empty or missing.
type ErrConfigJsonMissing struct {
Key string
}

func (e ErrConfigJsonMissing) Error() string {
return fmt.Sprintf(
"config JSON is not present when evaluating setting '%s'; returning the `defaultValue` parameter that you specified in your application",
e.Key,
)
}
Loading

0 comments on commit 6467281

Please sign in to comment.