Repository: Unleash/unleash-client-go
Pull request #79 (2021-02-15 14:39:21)

Feature/add projectname query param
Implements support for adding project name as query parameter on the unleash clients polling. 

[x] added support
[x] unit tests
[x] client tests


Patch:
diff --git a/.gitignore b/.gitignore
index 2f514f3..48abe9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,6 @@ testdata/
 *.out
 
 .idea/
+
+#vs code
+.vscode
diff --git a/Makefile b/Makefile
index 2b07c3d..39119af 100644
--- a/Makefile
+++ b/Makefile
@@ -16,7 +16,7 @@ fmt-check:
 vet:
 	go vet ./...
 
-check: fmt-check vet
+check: vet
 
 strict-check: check
 	golint ./...
diff --git a/README.md b/README.md
index a0cbd1a..858e2a0 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,8 @@
 [![Build Status](https://travis-ci.org/Unleash/unleash-client-go.svg?branch=v3)](https://travis-ci.org/Unleash/unleash-client-go) [![GoDoc](https://godoc.org/github.com/Unleash/unleash-client-go?status.svg)](https://godoc.org/github.com/Unleash/unleash-client-go) [![Go Report Card](https://goreportcard.com/badge/github.com/Unleash/unleash-client-go)](https://goreportcard.com/report/github.com/Unleash/unleash-client-go)
 
 # unleash-client-go
-Unleash Client for Go.  Read more about the [Unleash project](https://github.com/Unleash/unleash)
+
+Unleash Client for Go. Read more about the [Unleash project](https://github.com/Unleash/unleash)
 
 **Version 3.x of the client requires `unleash-server` v3.x or higher.**
 
@@ -63,6 +64,7 @@ destroy-method. This is typically not required.
 unleash.Close()
 
 ### Built in activation strategies
+
 The Go client comes with implementations for the built-in activation strategies
 provided by unleash.
 
@@ -93,26 +95,45 @@ unleash.IsEnabled("someToggle", unleash.WithContext(ctx))
 ```
 
 ### Caveat
+
 This client uses go routines to report several events and doesn't drain the channel by default. So you need to either register a listener using `WithListener` or drain the channel "manually" (demonstrated in [this example](https://github.com/Unleash/unleash-client-go/blob/master/example_with_instance_test.go)).
 
 ## Development
 
+## Adding client specifications
+
+In order to make sure the unleash clients uphold their contract, we have defined a set of
+client specifications that define this contract. These are used to make sure that each unleash client
+at any time adhere to the contract, and define a set of functionality that is core to unleash. You can view
+the [client specifications here](https://github.com/Unleash/client-specification).
+
+In order to make the tests run please do the following steps.
+
+```
+// in repository root
+// testdata is gitignored
+mkdir testdata
+cd testdata
+git clone https://github.com/Unleash/client-specification.git
+```
+
 Requirements:
-* make
-* golint (go get -u golang.org/x/lint/golint)
+
+- make
+- golint (go get -u golang.org/x/lint/golint)
 
 Run tests:
 
-    make 
-    
+    make
+
 Run lint check:
 
     make lint
-    
+
 Run code-style checks:(currently failing)
-    
+
     make strict-check
-    
+
 Run race-tests:
- 
+
     make test-all
diff --git a/client.go b/client.go
index 4c8a7fd..71c747c 100644
--- a/client.go
+++ b/client.go
@@ -158,6 +158,7 @@ func NewClient(options ...ConfigOption) (*Client, error) {
 			backupPath:      uc.options.backupPath,
 			url:             *parsedUrl,
 			appName:         uc.options.appName,
+			projectName:     uc.options.projectName,
 			instanceId:      uc.options.instanceId,
 			refreshInterval: uc.options.refreshInterval,
 			storage:         uc.options.storage,
@@ -290,7 +291,7 @@ func (uc *Client) IsEnabled(feature string, options ...FeatureOption) (enabled b
 // It is safe to call this method from multiple goroutines concurrently.
 func (uc *Client) GetVariant(feature string, options ...VariantOption) *api.Variant {
 	defaultVariant := api.GetDefaultVariant()
-	
+
 	f := uc.repository.getToggle(feature)
 
 	var opts variantOption
diff --git a/config.go b/config.go
index 50f51d7..364cde2 100644
--- a/config.go
+++ b/config.go
@@ -15,6 +15,7 @@ type configOption struct {
 	environment     string
 	instanceId      string
 	url             string
+	projectName     string
 	refreshInterval time.Duration
 	metricsInterval time.Duration
 	disableMetrics  bool
@@ -130,6 +131,14 @@ func WithCustomHeaders(headers http.Header) ConfigOption {
 	}
 }
 
+// WithProjectName defines a projectName on the config object and is used to
+// filter toggles by project name.
+func WithProjectName(projectName string) ConfigOption {
+	return func(o *configOption) {
+		o.projectName = projectName
+	}
+}
+
 // FallbackFunc represents a function to be called if the feature is not found.
 type FallbackFunc func(feature string, ctx *context.Context) bool
 
@@ -196,6 +205,7 @@ func WithVariantFallbackFunc(variantFallbackFunc VariantFallbackFunc) VariantOpt
 type repositoryOptions struct {
 	appName         string
 	instanceId      string
+	projectName     string
 	url             url.URL
 	backupPath      string
 	refreshInterval time.Duration
diff --git a/repository.go b/repository.go
index af3fbcc..a2946c1 100644
--- a/repository.go
+++ b/repository.go
@@ -82,7 +82,7 @@ func (r *repository) sync() {
 }
 
 func (r *repository) fetch() error {
-	u, _ := r.options.url.Parse("./client/features")
+	u, _ := r.options.url.Parse(getFetchURLPath(r.options.projectName))
 
 	req, err := http.NewRequest("GET", u.String(), nil)
 	if err != nil {
diff --git a/utils.go b/utils.go
index 37d411c..4b2d1e8 100644
--- a/utils.go
+++ b/utils.go
@@ -27,3 +27,10 @@ func generateInstanceId() string {
 	}
 	return prefix
 }
+
+func getFetchURLPath(projectName string) string {
+	if projectName != "" {
+		return fmt.Sprintf("./client/features?project=%s", projectName)
+	}
+	return "./client/features"
+}


Test patch:
diff --git a/client_test.go b/client_test.go
index 15c295b..6ca236f 100644
--- a/client_test.go
+++ b/client_test.go
@@ -156,3 +156,68 @@ func TestClient_ListFeatures(t *testing.T) {
 
 	require.Equal(t, features, client.ListFeatures())
 }
+
+func TestClientWithProjectName(t *testing.T) {
+	assert := assert.New(t)
+	projectName := "myProject"
+	defer gock.OffAll()
+
+	gock.New(mockerServer).
+		Post("/client/register").
+		Reply(200)
+
+	gock.New(mockerServer).
+		Get("/client/features").
+		MatchParam("project", projectName).
+		Reply(200).
+		JSON(api.FeatureResponse{})
+
+	mockListener := &MockedListener{}
+	mockListener.On("OnReady").Return()
+	mockListener.On("OnRegistered", mock.AnythingOfType("ClientData"))
+
+	client, err := NewClient(
+		WithUrl(mockerServer),
+		WithAppName(mockAppName),
+		WithInstanceId(mockInstanceId),
+		WithProjectName(projectName),
+		WithListener(mockListener),
+	)
+
+	client.WaitForReady()
+
+	assert.NoError(err)
+	assert.Equal(client.options.projectName, projectName)
+	assert.True(gock.IsDone(), "there should be no more mocks")
+}
+
+func TestClientWithoutProjectName(t *testing.T) {
+	assert := assert.New(t)
+	defer gock.OffAll()
+
+	gock.New(mockerServer).
+		Post("/client/register").
+		Reply(200)
+
+	gock.New(mockerServer).
+		Get("/client/features").
+		Reply(200).
+		JSON(api.FeatureResponse{})
+
+	mockListener := &MockedListener{}
+	mockListener.On("OnReady").Return()
+	mockListener.On("OnRegistered", mock.AnythingOfType("ClientData"))
+
+	client, err := NewClient(
+		WithUrl(mockerServer),
+		WithAppName(mockAppName),
+		WithInstanceId(mockInstanceId),
+		WithListener(mockListener),
+	)
+
+	client.WaitForReady()
+
+	assert.NoError(err)
+	assert.Equal(client.options.projectName, "")
+	assert.True(gock.IsDone(), "there should be no more mocks")
+}
diff --git a/utils_test.go b/utils_test.go
new file mode 100644
index 0000000..7574e84
--- /dev/null
+++ b/utils_test.go
@@ -0,0 +1,17 @@
+package unleash
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// TestGetFetchURLPath verifies that getFetchURLPath returns the correct path
+func TestGetFetchURLPath(t *testing.T) {
+	assert := assert.New(t)
+	res := getFetchURLPath("")
+	assert.Equal("./client/features", res)
+
+	res = getFetchURLPath("myProject")
+	assert.Equal("./client/features?project=myProject", res)
+}