Browse Source

/api/users/{id}/clusters endpoints added

Alexander Belanger 5 years ago
parent
commit
ce51261946

+ 2 - 6
internal/forms/user.go

@@ -1,10 +1,9 @@
 package forms
 
 import (
-	"gorm.io/gorm"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
-	"gopkg.in/yaml.v2"
+	"gorm.io/gorm"
 )
 
 // WriteUserForm is a generic form for write operations to the User model
@@ -42,16 +41,13 @@ type UpdateUserForm struct {
 // ToUser converts an UpdateUserForm to models.User by parsing the kubeconfig
 // and the allowed clusters to generate a list of ClusterConfigs.
 func (uuf *UpdateUserForm) ToUser() (*models.User, error) {
-	conf := kubernetes.KubeConfig{}
 	rawBytes := []byte(uuf.RawKubeConfig)
-	err := yaml.Unmarshal(rawBytes, &conf)
+	clusters, err := kubernetes.GetAllowedClusterConfigsFromBytes(rawBytes, uuf.AllowedClusters)
 
 	if err != nil {
 		return nil, err
 	}
 
-	clusters := conf.ToClusterConfigs(uuf.AllowedClusters)
-
 	return &models.User{
 		Model: gorm.Model{
 			ID: uuf.ID,

+ 63 - 8
internal/kubernetes/kubeconfig.go

@@ -1,9 +1,8 @@
 package kubernetes
 
 import (
-	"fmt"
-
 	"github.com/porter-dev/porter/internal/models"
+	"gopkg.in/yaml.v2"
 )
 
 // KubeConfigCluster represents the cluster field in a kubeconfig
@@ -36,11 +35,41 @@ type KubeConfig struct {
 	Users          []KubeConfigUser    `yaml:"users"`
 }
 
-// ToClusterConfigs converts a KubeConfig to a set of ClusterConfigExternals by
+// GetAllowedClusterConfigsFromBytes converts a raw string to a set of ClusterConfigs
+// by unmarshaling and calling (*KubeConfig).ToAllowedClusterConfigs
+func GetAllowedClusterConfigsFromBytes(bytes []byte, allowedClusters []string) ([]models.ClusterConfig, error) {
+	conf := KubeConfig{}
+	err := yaml.Unmarshal(bytes, &conf)
+
+	if err != nil {
+		return nil, err
+	}
+
+	clusters := conf.ToAllowedClusterConfigs(allowedClusters)
+
+	return clusters, nil
+}
+
+// GetAllClusterConfigsFromBytes converts a raw string to a set of ClusterConfigs
+// by unmarshaling and calling (*KubeConfig).ToAllClusterConfigs
+func GetAllClusterConfigsFromBytes(bytes []byte) ([]models.ClusterConfig, error) {
+	conf := KubeConfig{}
+	err := yaml.Unmarshal(bytes, &conf)
+
+	if err != nil {
+		return nil, err
+	}
+
+	clusters := conf.ToAllClusterConfigs()
+
+	return clusters, nil
+}
+
+// ToAllowedClusterConfigs converts a KubeConfig to a set of ClusterConfigs by
 // joining users and clusters on the context.
 //
 // It accepts a list of cluster names that the user wishes to connect to
-func (k *KubeConfig) ToClusterConfigs(allowedClusters []string) []models.ClusterConfig {
+func (k *KubeConfig) ToAllowedClusterConfigs(allowedClusters []string) []models.ClusterConfig {
 	clusters := make([]models.ClusterConfig, 0)
 
 	// convert clusters, contexts, and users to maps for fast lookup
@@ -51,8 +80,6 @@ func (k *KubeConfig) ToClusterConfigs(allowedClusters []string) []models.Cluster
 	// put allowed clusters in map
 	aClusterMap := createAllowedClusterMap(allowedClusters)
 
-	fmt.Println(allowedClusters, aClusterMap)
-
 	// iterate through context maps and link to a user-cluster pair
 	for contextName, context := range contextMap {
 		userName := context.Context.User
@@ -63,8 +90,6 @@ func (k *KubeConfig) ToClusterConfigs(allowedClusters []string) []models.Cluster
 		// make sure the cluster is "allowed"
 		_, aClusterFound := aClusterMap[clusterName]
 
-		fmt.Println(userFound, clusterFound, aClusterFound)
-
 		if userFound && clusterFound && aClusterFound {
 			clusters = append(clusters, models.ClusterConfig{
 				Name:    clusterName,
@@ -78,6 +103,36 @@ func (k *KubeConfig) ToClusterConfigs(allowedClusters []string) []models.Cluster
 	return clusters
 }
 
+// ToAllClusterConfigs converts a KubeConfig to a set of ClusterConfigs by
+// joining users and clusters on the context.
+func (k *KubeConfig) ToAllClusterConfigs() []models.ClusterConfig {
+	clusters := make([]models.ClusterConfig, 0)
+
+	// convert clusters, contexts, and users to maps for fast lookup
+	clusterMap := k.createClusterMap()
+	contextMap := k.createContextMap()
+	userMap := k.createUserMap()
+
+	// iterate through context maps and link to a user-cluster pair
+	for contextName, context := range contextMap {
+		userName := context.Context.User
+		clusterName := context.Context.Cluster
+		_, userFound := userMap[userName]
+		cluster, clusterFound := clusterMap[clusterName]
+
+		if userFound && clusterFound {
+			clusters = append(clusters, models.ClusterConfig{
+				Name:    clusterName,
+				Server:  cluster.Cluster.Server,
+				Context: contextName,
+				User:    userName,
+			})
+		}
+	}
+
+	return clusters
+}
+
 // createAllowedClusterMap creates a map from a cluster name to a KubeConfigCluster object
 func createAllowedClusterMap(clusters []string) map[string]string {
 	aClusterMap := make(map[string]string)

+ 3 - 3
internal/kubernetes/kubeconfig_test.go

@@ -65,7 +65,7 @@ func TestToClusterConfigsMissingFields(t *testing.T) {
 			t.Errorf("Testing: %s, Error: %v\n", c.msg, err)
 		}
 
-		res := conf.ToClusterConfigs(c.allowedClusters)
+		res := conf.ToAllowedClusterConfigs(c.allowedClusters)
 
 		isEqual := reflect.DeepEqual(c.expected, res)
 
@@ -95,7 +95,7 @@ func TestToClusterConfigsNoAllowedClusters(t *testing.T) {
 			t.Errorf("Testing: %s, Error: %v\n", c.msg, err)
 		}
 
-		res := conf.ToClusterConfigs(c.allowedClusters)
+		res := conf.ToAllowedClusterConfigs(c.allowedClusters)
 
 		isEqual := reflect.DeepEqual(c.expected, res)
 
@@ -131,7 +131,7 @@ func TestToClusterConfigsBasic(t *testing.T) {
 			t.Errorf("Testing: %s, Error: %v\n", c.msg, err)
 		}
 
-		res := conf.ToClusterConfigs(c.allowedClusters)
+		res := conf.ToAllowedClusterConfigs(c.allowedClusters)
 
 		isEqual := reflect.DeepEqual(c.expected, res)
 

+ 74 - 6
server/api/user_handler.go

@@ -7,6 +7,8 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/porter-dev/porter/internal/kubernetes"
+
 	"gorm.io/gorm"
 
 	"github.com/go-chi/chi"
@@ -44,23 +46,71 @@ func (app *App) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
 // HandleReadUser returns an externalized User (models.UserExternal)
 // based on an ID
 func (app *App) HandleReadUser(w http.ResponseWriter, r *http.Request) {
-	id, err := strconv.ParseUint(chi.URLParam(r, "id"), 0, 64)
+	user, err := app.readUser(w, r)
 
-	if err != nil || id == 0 {
+	// error already handled by helper
+	if err != nil {
+		return
+	}
+
+	extUser := user.Externalize()
+
+	if err := json.NewEncoder(w).Encode(extUser); err != nil {
 		app.handleErrorFormDecoding(err, ErrUserDecode, w)
 		return
 	}
 
-	user, err := app.repo.User.ReadUser(uint(id))
+	w.WriteHeader(http.StatusOK)
+}
+
+// HandleReadUserClusters returns the externalized User.Clusters (models.ClusterConfigs)
+// based on a user ID
+func (app *App) HandleReadUserClusters(w http.ResponseWriter, r *http.Request) {
+	user, err := app.readUser(w, r)
 
+	// error already handled by helper
 	if err != nil {
-		app.handleErrorRead(err, ErrUserDataRead, w)
 		return
 	}
 
-	extUser := user.Externalize()
+	extClusters := make([]models.ClusterConfigExternal, 0)
 
-	if err := json.NewEncoder(w).Encode(extUser); err != nil {
+	for _, cluster := range user.Clusters {
+		extClusters = append(extClusters, *cluster.Externalize())
+	}
+
+	if err := json.NewEncoder(w).Encode(extClusters); err != nil {
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+}
+
+// HandleReadUserClustersAll returns all models.ClusterConfigs parsed from a KubeConfig
+// that is attached to a specific user, identified through the user ID
+func (app *App) HandleReadUserClustersAll(w http.ResponseWriter, r *http.Request) {
+	user, err := app.readUser(w, r)
+
+	// error already handled by helper
+	if err != nil {
+		return
+	}
+
+	clusters, err := kubernetes.GetAllClusterConfigsFromBytes(user.RawKubeConfig)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
+		return
+	}
+
+	extClusters := make([]models.ClusterConfigExternal, 0)
+
+	for _, cluster := range clusters {
+		extClusters = append(extClusters, *cluster.Externalize())
+	}
+
+	if err := json.NewEncoder(w).Encode(extClusters); err != nil {
 		app.handleErrorFormDecoding(err, ErrUserDecode, w)
 		return
 	}
@@ -184,6 +234,24 @@ func (app *App) writeUser(
 	return user, nil
 }
 
+func (app *App) readUser(w http.ResponseWriter, r *http.Request) (*models.User, error) {
+	id, err := strconv.ParseUint(chi.URLParam(r, "id"), 0, 64)
+
+	if err != nil || id == 0 {
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
+		return nil, err
+	}
+
+	user, err := app.repo.User.ReadUser(uint(id))
+
+	if err != nil {
+		app.handleErrorRead(err, ErrUserDataRead, w)
+		return nil, err
+	}
+
+	return user, nil
+}
+
 func doesUserExist(repo *repository.Repository, user *models.User) *HTTPError {
 	user, err := repo.User.ReadUserByEmail(user.Email)
 

+ 223 - 200
server/api/user_handler_test.go

@@ -42,16 +42,50 @@ func initApi(canQuery bool) (*api.App, *repository.Repository) {
 	return api.New(logger, repo, validator), repo
 }
 
+func testUserRequest(t *testing.T, c userTest) {
+	// create a mock API
+	api, repo := initApi(c.canQuery)
+	r := router.New(api)
+
+	if c.init != nil {
+		c.init(repo)
+	}
+
+	req, err := http.NewRequest(
+		c.method,
+		c.endpoint,
+		strings.NewReader(c.body),
+	)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	rr := httptest.NewRecorder()
+	r.ServeHTTP(rr, req)
+
+	// first, check that the status matches
+	if status := rr.Code; status != c.expStatus {
+		t.Errorf("%s, handler returned wrong status code: got %v want %v",
+			c.msg, status, c.expStatus)
+	}
+
+	// if there's a validator, call it
+	for _, validate := range c.validators {
+		validate(rr, c, r, t)
+	}
+}
+
 type userTest struct {
 	init func(repo *repository.Repository)
 	msg,
 	method,
 	endpoint,
 	body string
-	expStatus int
-	expBody   string
-	canQuery  bool
-	validate  func(r *chi.Mux, t *testing.T)
+	expStatus  int
+	expBody    string
+	canQuery   bool
+	validators []func(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T)
 }
 
 var createUserTests = []userTest{
@@ -78,6 +112,9 @@ var createUserTests = []userTest{
 		expStatus: http.StatusUnprocessableEntity,
 		expBody:   `{"code":601,"errors":["email validation failed"]}`,
 		canQuery:  true,
+		validators: []func(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T){
+			BasicBodyValidator,
+		},
 	},
 	userTest{
 		msg:      "Create user missing field",
@@ -89,6 +126,9 @@ var createUserTests = []userTest{
 		expStatus: http.StatusUnprocessableEntity,
 		expBody:   `{"code":601,"errors":["required validation failed"]}`,
 		canQuery:  true,
+		validators: []func(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T){
+			BasicBodyValidator,
+		},
 	},
 	userTest{
 		msg:      "Create user cannot write to db",
@@ -101,6 +141,9 @@ var createUserTests = []userTest{
 		expStatus: http.StatusInternalServerError,
 		expBody:   `{"code":500,"errors":["could not read from database"]}`,
 		canQuery:  false,
+		validators: []func(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T){
+			BasicBodyValidator,
+		},
 	},
 	userTest{
 		init: func(repo *repository.Repository) {
@@ -119,41 +162,15 @@ var createUserTests = []userTest{
 		expStatus: http.StatusUnprocessableEntity,
 		expBody:   `{"code":601,"errors":["email already taken"]}`,
 		canQuery:  true,
+		validators: []func(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T){
+			BasicBodyValidator,
+		},
 	},
 }
 
 func TestHandleCreateUser(t *testing.T) {
 	for _, c := range createUserTests {
-		// create a mock API
-		api, repo := initApi(c.canQuery)
-		r := router.New(api)
-
-		if c.init != nil {
-			c.init(repo)
-		}
-
-		req, err := http.NewRequest(
-			c.method,
-			c.endpoint,
-			strings.NewReader(c.body),
-		)
-
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		rr := httptest.NewRecorder()
-		r.ServeHTTP(rr, req)
-
-		if status := rr.Code; status != c.expStatus {
-			t.Errorf("%s, handler returned wrong status code: got %v want %v",
-				c.msg, status, c.expStatus)
-		}
-
-		if body := rr.Body.String(); body != c.expBody {
-			t.Errorf("%s, handler returned wrong body: got %v want %v",
-				c.msg, body, c.expBody)
-		}
+		testUserRequest(t, c)
 	}
 }
 
@@ -181,6 +198,9 @@ var readUserTests = []userTest{
 		expStatus: http.StatusOK,
 		expBody:   `{"id":1,"email":"belanger@getporter.dev","clusters":[{"name":"cluster-test","server":"https://localhost","context":"context-test","user":"test-admin"}],"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: default\nclusters:\n- cluster:\n    server: https://localhost\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\nusers:\n- name: test-admin"}`,
 		canQuery:  true,
+		validators: []func(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T){
+			UserModelBodyValidator,
+		},
 	},
 	userTest{
 		init: func(repo *repository.Repository) {
@@ -196,6 +216,9 @@ var readUserTests = []userTest{
 		expStatus: http.StatusBadRequest,
 		expBody:   `{"code":600,"errors":["could not process request"]}`,
 		canQuery:  true,
+		validators: []func(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T){
+			BasicBodyValidator,
+		},
 	},
 	userTest{
 		init: func(repo *repository.Repository) {
@@ -211,56 +234,80 @@ var readUserTests = []userTest{
 		expStatus: http.StatusNotFound,
 		expBody:   `{"code":602,"errors":["could not find requested object"]}`,
 		canQuery:  true,
+		validators: []func(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T){
+			BasicBodyValidator,
+		},
 	},
 }
 
 func TestHandleReadUser(t *testing.T) {
 	for _, c := range readUserTests {
-		// create a mock API
-		api, repo := initApi(c.canQuery)
-		r := router.New(api)
-
-		if c.init != nil {
-			c.init(repo)
-		}
-
-		req, err := http.NewRequest(
-			c.method,
-			c.endpoint,
-			strings.NewReader(c.body),
-		)
-
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		rr := httptest.NewRecorder()
-		r.ServeHTTP(rr, req)
-
-		if status := rr.Code; status != c.expStatus {
-			t.Errorf("%s, handler returned wrong status code: got %v want %v",
-				c.msg, status, c.expStatus)
-		}
-
-		if status := rr.Code; status == 200 {
-			// if status is expected to be 200, parse the body for UserExternal
-			gotBody := &models.UserExternal{}
-			expBody := &models.UserExternal{}
-
-			json.Unmarshal(rr.Body.Bytes(), gotBody)
-			json.Unmarshal([]byte(c.expBody), expBody)
-
-			if !reflect.DeepEqual(gotBody, expBody) {
-				t.Errorf("%s, handler returned wrong body: got %v want %v",
-					c.msg, gotBody, expBody)
-			}
-		} else {
-			// if status is expected to not be 200, look for error
-			if body := rr.Body.String(); body != c.expBody {
-				t.Errorf("%s, handler returned wrong body: got %v want %v",
-					c.msg, body, c.expBody)
-			}
-		}
+		testUserRequest(t, c)
+	}
+}
+
+var readUserClustersTests = []userTest{
+	userTest{
+		init: func(repo *repository.Repository) {
+			repo.User.CreateUser(&models.User{
+				Email:    "belanger@getporter.dev",
+				Password: "hello",
+				Clusters: []models.ClusterConfig{
+					models.ClusterConfig{
+						Name:    "cluster-test",
+						Server:  "https://localhost",
+						Context: "context-test",
+						User:    "test-admin",
+					},
+				},
+				RawKubeConfig: []byte("apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: default\nclusters:\n- cluster:\n    server: https://localhost\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\nusers:\n- name: test-admin"),
+			})
+		},
+		msg:       "Read user successful",
+		method:    "GET",
+		endpoint:  "/api/users/1/clusters",
+		body:      "",
+		expStatus: http.StatusOK,
+		expBody:   `[{"name":"cluster-test","server":"https://localhost","context":"context-test","user":"test-admin"}]`,
+		canQuery:  true,
+		validators: []func(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T){
+			ClusterBodyValidator,
+		},
+	},
+}
+
+func TestHandleReadUserClusters(t *testing.T) {
+	for _, c := range readUserClustersTests {
+		testUserRequest(t, c)
+	}
+}
+
+var readUserClustersAllTests = []userTest{
+	userTest{
+		init: func(repo *repository.Repository) {
+			repo.User.CreateUser(&models.User{
+				Email:         "belanger@getporter.dev",
+				Password:      "hello",
+				Clusters:      []models.ClusterConfig{},
+				RawKubeConfig: []byte("apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: default\nclusters:\n- cluster:\n    server: https://localhost\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\nusers:\n- name: test-admin"),
+			})
+		},
+		msg:       "Read user successful",
+		method:    "GET",
+		endpoint:  "/api/users/1/clusters/all",
+		body:      "",
+		expStatus: http.StatusOK,
+		expBody:   `[{"name":"cluster-test","server":"https://localhost","context":"context-test","user":"test-admin"}]`,
+		canQuery:  true,
+		validators: []func(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T){
+			ClusterBodyValidator,
+		},
+	},
+}
+
+func TestHandleReadUserClustersAll(t *testing.T) {
+	for _, c := range readUserClustersAllTests {
+		testUserRequest(t, c)
 	}
 }
 
@@ -279,30 +326,32 @@ var updateUserTests = []userTest{
 		expStatus: http.StatusNoContent,
 		expBody:   "",
 		canQuery:  true,
-		validate: func(r *chi.Mux, t *testing.T) {
-			req, err := http.NewRequest(
-				"GET",
-				"/api/users/1",
-				strings.NewReader(""),
-			)
-
-			if err != nil {
-				t.Fatal(err)
-			}
-
-			rr := httptest.NewRecorder()
-			r.ServeHTTP(rr, req)
-
-			gotBody := &models.UserExternal{}
-			expBody := &models.UserExternal{}
-
-			json.Unmarshal(rr.Body.Bytes(), gotBody)
-			json.Unmarshal([]byte(`{"id":1,"email":"belanger@getporter.dev","clusters":[],"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: default\nclusters:\n- cluster:\n    server: https://localhost\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\nusers:\n- name: test-admin"}`), expBody)
-
-			if !reflect.DeepEqual(gotBody, expBody) {
-				t.Errorf("%s, handler returned wrong body: got %v want %v",
-					"validator failed", gotBody, expBody)
-			}
+		validators: []func(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T){
+			func(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T) {
+				req, err := http.NewRequest(
+					"GET",
+					"/api/users/1",
+					strings.NewReader(""),
+				)
+
+				if err != nil {
+					t.Fatal(err)
+				}
+
+				rr2 := httptest.NewRecorder()
+				r.ServeHTTP(rr2, req)
+
+				gotBody := &models.UserExternal{}
+				expBody := &models.UserExternal{}
+
+				json.Unmarshal(rr2.Body.Bytes(), gotBody)
+				json.Unmarshal([]byte(`{"id":1,"email":"belanger@getporter.dev","clusters":[],"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: default\nclusters:\n- cluster:\n    server: https://localhost\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\nusers:\n- name: test-admin"}`), expBody)
+
+				if !reflect.DeepEqual(gotBody, expBody) {
+					t.Errorf("%s, handler returned wrong body: got %v want %v",
+						"validator failed", gotBody, expBody)
+				}
+			},
 		},
 	},
 	userTest{
@@ -319,45 +368,15 @@ var updateUserTests = []userTest{
 		expStatus: http.StatusBadRequest,
 		expBody:   `{"code":600,"errors":["could not process request"]}`,
 		canQuery:  true,
+		validators: []func(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T){
+			BasicBodyValidator,
+		},
 	},
 }
 
 func TestHandleUpdateUser(t *testing.T) {
 	for _, c := range updateUserTests {
-		// create a mock API
-		api, repo := initApi(c.canQuery)
-		r := router.New(api)
-
-		if c.init != nil {
-			c.init(repo)
-		}
-
-		req, err := http.NewRequest(
-			c.method,
-			c.endpoint,
-			strings.NewReader(c.body),
-		)
-
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		rr := httptest.NewRecorder()
-		r.ServeHTTP(rr, req)
-
-		if status := rr.Code; status != c.expStatus {
-			t.Errorf("%s, handler returned wrong status code: got %v want %v",
-				c.msg, status, c.expStatus)
-		}
-
-		if body := rr.Body.String(); body != c.expBody {
-			t.Errorf("%s, handler returned wrong body: got %v want %v",
-				c.msg, body, c.expBody)
-		}
-
-		if c.validate != nil {
-			c.validate(r, t)
-		}
+		testUserRequest(t, c)
 	}
 }
 
@@ -376,74 +395,78 @@ var deleteUserTests = []userTest{
 		expStatus: http.StatusNoContent,
 		expBody:   "",
 		canQuery:  true,
-		validate: func(r *chi.Mux, t *testing.T) {
-			req, err := http.NewRequest(
-				"GET",
-				"/api/users/1",
-				strings.NewReader(""),
-			)
-
-			if err != nil {
-				t.Fatal(err)
-			}
-
-			rr := httptest.NewRecorder()
-			r.ServeHTTP(rr, req)
-
-			gotBody := &models.UserExternal{}
-			expBody := &models.UserExternal{}
-
-			if status := rr.Code; status != 404 {
-				t.Errorf("DELETE user validation, handler returned wrong status code: got %v want %v",
-					status, 404)
-			}
-
-			json.Unmarshal(rr.Body.Bytes(), gotBody)
-			json.Unmarshal([]byte(`{"code":602,"errors":["could not find requested object"]}`), expBody)
-
-			if !reflect.DeepEqual(gotBody, expBody) {
-				t.Errorf("%s, handler returned wrong body: got %v want %v",
-					"validator failed", gotBody, expBody)
-			}
+		validators: []func(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T){
+			func(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T) {
+				req, err := http.NewRequest(
+					"GET",
+					"/api/users/1",
+					strings.NewReader(""),
+				)
+
+				if err != nil {
+					t.Fatal(err)
+				}
+
+				rr2 := httptest.NewRecorder()
+
+				r.ServeHTTP(rr2, req)
+
+				gotBody := &models.UserExternal{}
+				expBody := &models.UserExternal{}
+
+				if status := rr2.Code; status != 404 {
+					t.Errorf("DELETE user validation, handler returned wrong status code: got %v want %v",
+						status, 404)
+				}
+
+				json.Unmarshal(rr2.Body.Bytes(), gotBody)
+				json.Unmarshal([]byte(`{"code":602,"errors":["could not find requested object"]}`), expBody)
+
+				if !reflect.DeepEqual(gotBody, expBody) {
+					t.Errorf("%s, handler returned wrong body: got %v want %v",
+						"validator failed", gotBody, expBody)
+				}
+			},
 		},
 	},
 }
 
 func TestHandleDeleteUser(t *testing.T) {
 	for _, c := range deleteUserTests {
-		// create a mock API
-		api, repo := initApi(c.canQuery)
-		r := router.New(api)
-
-		if c.init != nil {
-			c.init(repo)
-		}
-
-		req, err := http.NewRequest(
-			c.method,
-			c.endpoint,
-			strings.NewReader(c.body),
-		)
-
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		rr := httptest.NewRecorder()
-		r.ServeHTTP(rr, req)
-
-		if status := rr.Code; status != c.expStatus {
-			t.Errorf("%s, handler returned wrong status code: got %v want %v",
-				c.msg, status, c.expStatus)
-		}
-
-		if body := rr.Body.String(); body != c.expBody {
-			t.Errorf("%s, handler returned wrong body: got %v want %v",
-				c.msg, body, c.expBody)
-		}
-
-		if c.validate != nil {
-			c.validate(r, t)
-		}
+		testUserRequest(t, c)
+	}
+}
+
+func BasicBodyValidator(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T) {
+	if body := rr.Body.String(); body != c.expBody {
+		t.Errorf("%s, handler returned wrong body: got %v want %v",
+			c.msg, body, c.expBody)
+	}
+}
+
+func UserModelBodyValidator(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T) {
+	gotBody := &models.UserExternal{}
+	expBody := &models.UserExternal{}
+
+	json.Unmarshal(rr.Body.Bytes(), gotBody)
+	json.Unmarshal([]byte(c.expBody), expBody)
+
+	if !reflect.DeepEqual(gotBody, expBody) {
+		t.Errorf("%s, handler returned wrong body: got %v want %v",
+			c.msg, gotBody, expBody)
+	}
+}
+
+func ClusterBodyValidator(rr *httptest.ResponseRecorder, c userTest, r *chi.Mux, t *testing.T) {
+	// if status is expected to be 200, parse the body for UserExternal
+	gotBody := &[]models.ClusterConfigExternal{}
+	expBody := &[]models.ClusterConfigExternal{}
+
+	json.Unmarshal(rr.Body.Bytes(), gotBody)
+	json.Unmarshal([]byte(c.expBody), expBody)
+
+	if !reflect.DeepEqual(gotBody, expBody) {
+		t.Errorf("%s, handler returned wrong body: got %v want %v",
+			c.msg, gotBody, expBody)
 	}
 }

+ 3 - 1
server/router/router.go

@@ -16,9 +16,11 @@ func New(a *api.App) *chi.Mux {
 		r.Use(middleware.ContentTypeJSON)
 
 		// /api/users routes
+		r.Method("GET", "/users/{id}", requestlog.NewHandler(a.HandleReadUser, l))
+		r.Method("GET", "/users/{id}/clusters", requestlog.NewHandler(a.HandleReadUserClusters, l))
+		r.Method("GET", "/users/{id}/clusters/all", requestlog.NewHandler(a.HandleReadUserClustersAll, l))
 		r.Method("POST", "/users", requestlog.NewHandler(a.HandleCreateUser, l))
 		r.Method("PUT", "/users/{id}", requestlog.NewHandler(a.HandleUpdateUser, l))
-		r.Method("GET", "/users/{id}", requestlog.NewHandler(a.HandleReadUser, l))
 		r.Method("DELETE", "/users/{id}", requestlog.NewHandler(a.HandleDeleteUser, l))
 	})