Repository: Unleash/unleash-client-go
Pull request #29 (2018-09-14 22:48:15)

Testing/implement client spec test
Looks like the Travis build is failing despite everything passing locally so I'll try to look at that when I get a chance.

Besides that, all of the client spec tests are now passing. I only had to fix one bug 1e6f2ea which was a pleasant surprise.

The way the test runner works is as follows:
 - Create and run the suite in `TestClientSpecificationSuite`
 - This implicitly calls `SetupTest()`
 - Which loads all the files and parses the JSON into an array of `TestDefinitions`
 - Each TestDefinition is executed which involves:
   - Create a mocked client
   - Iterate through the test cases and run them, passing them the mocked client
   - Compare results

ivarconr: looks like https://travis-ci.org/Unleash/unleash-client-go/builds/428832620#L495 is missing a mock for `/client/features' 
ivarconr: Cool that you managed to use the client-specs :100:  . Will be an awesome tool when implementing support for 4.x (grouped strategies). :+1: 
jrbarron: Agreed, I'll fix Travis before merging this one.

Patch:
diff --git a/.travis.yml b/.travis.yml
index 47af96d..7dc3dc5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,6 +6,9 @@ go:
 
 go_import_path: github.com/Unleash/unleash-client-go/v3
 
+before_install:
+  - git clone https://github.com/Unleash/client-specification.git ./testdata/client-specification
+
 script:
  - go get -t -v ./...
  - go vet ./...
diff --git a/client.go b/client.go
index 19b4cd9..0ee5cd1 100644
--- a/client.go
+++ b/client.go
@@ -36,10 +36,12 @@ type Client struct {
 	metricsListener    MetricListener
 	repositoryListener RepositoryListener
 	ready              chan bool
+	onReady            chan bool
 	closed             chan bool
 	count              chan metric
 	sent               chan MetricsData
 	registered         chan ClientData
+	isReady            bool
 }
 
 type errorChannels struct {
@@ -84,6 +86,7 @@ func NewClient(options ...ConfigOption) (*Client, error) {
 			strategies:      []strategy.Strategy{},
 		},
 		errorChannels: errChannels,
+		onReady:       make(chan bool, 1),
 		ready:         make(chan bool, 1),
 		count:         make(chan metric),
 		sent:          make(chan MetricsData),
@@ -192,6 +195,7 @@ func (uc *Client) sync() {
 				uc.errorListener.OnWarning(w)
 			}
 		case <-uc.ready:
+			uc.onReady <- true
 			if uc.repositoryListener != nil {
 				uc.repositoryListener.OnReady()
 			}
@@ -237,6 +241,10 @@ func (uc Client) IsEnabled(feature string, options ...FeatureOption) (enabled bo
 		return false
 	}
 
+	if len(f.Strategies) == 0 {
+		return f.Enabled
+	}
+
 	for _, s := range f.Strategies {
 		foundStrategy := uc.getStrategy(s.Name)
 		if foundStrategy == nil {
@@ -299,3 +307,11 @@ func (uc Client) getStrategy(name string) strategy.Strategy {
 	}
 	return nil
 }
+
+// WaitForReady will block until the internal repository has loaded the feature toggles from the
+// storage engine. It will return immediately if the repository is already ready.
+func (uc *Client) WaitForReady() {
+	if !uc.isReady {
+		uc.isReady = <-uc.onReady
+	}
+}
diff --git a/unleash.go b/unleash.go
index e860390..7d2b15e 100644
--- a/unleash.go
+++ b/unleash.go
@@ -48,3 +48,8 @@ func Initialize(options ...ConfigOption) (err error) {
 func Close() error {
 	return defaultClient.Close()
 }
+
+// WaitForReady will block until the default client is ready or return immediately.
+func WaitForReady() {
+	defaultClient.WaitForReady()
+}


Test patch:
diff --git a/spec_test.go b/spec_test.go
new file mode 100644
index 0000000..9b47c90
--- /dev/null
+++ b/spec_test.go
@@ -0,0 +1,116 @@
+package unleash
+
+import (
+	"encoding/json"
+	"github.com/Unleash/unleash-client-go/v3/context"
+	"github.com/Unleash/unleash-client-go/v3/internal/api"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/suite"
+	"gopkg.in/h2non/gock.v1"
+	"os"
+	"path/filepath"
+	"testing"
+	)
+
+const mockHost = "http://unleash-apu"
+const specFolder = "./testdata/client-specification/specifications"
+
+var specIndex = filepath.Join(specFolder, "index.json")
+
+type TestState struct {
+	Version  int           `json:"version"`
+	Features []api.Feature `json:"features"`
+}
+
+type TestCase struct {
+	Description    string          `json:"description"`
+	Context        context.Context `json:"context"`
+	ToggleName     string          `json:"toggleName"`
+	ExpectedResult bool            `json:"expectedResult"`
+}
+
+func (tc TestCase) RunWithClient(client *Client) func(*testing.T) {
+	return func(t *testing.T) {
+		client.WaitForReady()
+		result := client.IsEnabled(tc.ToggleName, WithContext(tc.Context))
+		assert.Equal(t, tc.ExpectedResult, result)
+	}
+}
+
+type TestDefinition struct {
+	Name  string    `json:"name"`
+	State TestState `json:"state"`
+	Tests []TestCase `json:"tests"`
+}
+
+func (td TestDefinition) Mock() (*Client, error) {
+	gock.New(mockHost).
+		Get("/client/features").
+		Reply(200).
+		JSON(api.FeatureResponse{
+			Response: api.Response{
+				Version: td.State.Version,
+			},
+			Features: td.State.Features,
+		})
+
+	return NewClient(
+		WithUrl(mockHost),
+		WithAppName("clientSpecificationTest"),
+		WithListener(MockedListener{}),
+	)
+}
+
+func (td TestDefinition) Unmock() () {
+	gock.OffAll()
+}
+
+func (td TestDefinition) Run(t *testing.T) {
+	for _, test := range td.Tests {
+		client, err := td.Mock()
+		assert.NoError(t, err)
+		t.Run(test.Description, test.RunWithClient(client))
+		td.Unmock()
+	}
+}
+
+type ClientSpecificationSuite struct {
+	suite.Suite
+	definitions []TestDefinition
+}
+
+func (s ClientSpecificationSuite) loadTestDefinition(testFile string) TestDefinition {
+	test, err := os.Open(filepath.Join(specFolder, testFile))
+	s.NoError(err)
+	defer test.Close()
+	var testDef TestDefinition
+	dec := json.NewDecoder(test)
+	err = dec.Decode(&testDef)
+	s.NoError(err)
+	return testDef
+}
+
+func (s *ClientSpecificationSuite) SetupTest() {
+	index, err := os.Open(specIndex)
+	s.NoError(err)
+	defer index.Close()
+
+	var testFiles []string
+	dec := json.NewDecoder(index)
+	err = dec.Decode(&testFiles)
+	s.NoError(err)
+
+	for _, testFile := range testFiles {
+		s.definitions = append(s.definitions, s.loadTestDefinition(testFile))
+	}
+}
+
+func (s ClientSpecificationSuite) TestClientSpecification() {
+	for _, def := range s.definitions {
+		s.T().Run(def.Name, def.Run)
+	}
+}
+
+func TestClientSpecificationSuite(t *testing.T) {
+	suite.Run(t, new(ClientSpecificationSuite))
+}