github.com/confluentinc/cli@v1.100.0/test/cli_test.go (about)

     1  package test
     2  
     3  import (
     4  	"encoding/json"
     5  	"flag"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"net/url"
    12  	"os"
    13  	"os/exec"
    14  	"path"
    15  	"reflect"
    16  	"regexp"
    17  	"runtime"
    18  	"sort"
    19  	"strconv"
    20  	"strings"
    21  	"testing"
    22  
    23  	linkv1 "github.com/confluentinc/cc-structs/kafka/clusterlink/v1"
    24  
    25  	"github.com/confluentinc/cli/internal/pkg/utils"
    26  
    27  	"github.com/confluentinc/cli/internal/pkg/errors"
    28  
    29  	"github.com/gogo/protobuf/proto"
    30  	"github.com/stretchr/testify/require"
    31  	"github.com/stretchr/testify/suite"
    32  
    33  	"github.com/confluentinc/bincover"
    34  	corev1 "github.com/confluentinc/cc-structs/kafka/core/v1"
    35  	orgv1 "github.com/confluentinc/cc-structs/kafka/org/v1"
    36  	productv1 "github.com/confluentinc/cc-structs/kafka/product/core/v1"
    37  	schedv1 "github.com/confluentinc/cc-structs/kafka/scheduler/v1"
    38  	utilv1 "github.com/confluentinc/cc-structs/kafka/util/v1"
    39  	opv1 "github.com/confluentinc/cc-structs/operator/v1"
    40  	"github.com/confluentinc/ccloud-sdk-go"
    41  
    42  	"github.com/confluentinc/cli/internal/pkg/config"
    43  	v3 "github.com/confluentinc/cli/internal/pkg/config/v3"
    44  )
    45  
    46  var (
    47  	noRebuild           = flag.Bool("no-rebuild", false, "skip rebuilding CLI if it already exists")
    48  	update              = flag.Bool("update", false, "update golden files")
    49  	debug               = flag.Bool("debug", true, "enable verbose output")
    50  	skipSsoBrowserTests = flag.Bool("skip-sso-browser-tests", false, "If flag is preset, run the tests that require a web browser.")
    51  	ssoTestEmail        = *flag.String("sso-test-user-email", "ziru+paas-integ-sso@confluent.io", "The email of an sso enabled test user.")
    52  	ssoTestPassword     = *flag.String("sso-test-user-password", "aWLw9eG+F", "The password for the sso enabled test user.")
    53  	// this connection is preconfigured in Auth0 to hit a test Okta account
    54  	ssoTestConnectionName = *flag.String("sso-test-connection-name", "confluent-dev", "The Auth0 SSO connection name.")
    55  	// browser tests by default against devel
    56  	ssoTestLoginUrl  = *flag.String("sso-test-login-url", "https://devel.cpdev.cloud", "The login url to use for the sso browser test.")
    57  	cover            = false
    58  	ccloudTestBin    = ccloudTestBinNormal
    59  	confluentTestBin = confluentTestBinNormal
    60  	covCollector     *bincover.CoverageCollector
    61  	environments     = []*orgv1.Account{{Id: "a-595", Name: "default"}, {Id: "not-595", Name: "other"}}
    62  )
    63  
    64  const (
    65  	confluentTestBinNormal = "confluent_test"
    66  	ccloudTestBinNormal    = "ccloud_test"
    67  	ccloudTestBinRace      = "ccloud_test_race"
    68  	confluentTestBinRace   = "confluent_test_race"
    69  	mergedCoverageFilename = "integ_coverage.txt"
    70  )
    71  
    72  // CLITest represents a test configuration
    73  type CLITest struct {
    74  	// Name to show in go test output; defaults to args if not set
    75  	name string
    76  	// The CLI command being tested; this is a string of args and flags passed to the binary
    77  	args string
    78  	// The set of environment variables to be set when the CLI is run
    79  	env []string
    80  	// "default" if you need to login, or "" otherwise
    81  	login string
    82  	// The kafka cluster ID to "use"
    83  	useKafka string
    84  	// The API Key to set as Kafka credentials
    85  	authKafka string
    86  	// Name of a golden output fixture containing expected output
    87  	fixture string
    88  	// True iff fixture represents a regex
    89  	regex bool
    90  	// Fixed string to check if output contains
    91  	contains string
    92  	// Fixed string to check that output does not contain
    93  	notContains string
    94  	// Expected exit code (e.g., 0 for success or 1 for failure)
    95  	wantErrCode int
    96  	// If true, don't reset the config/state between tests to enable testing CLI workflows
    97  	workflow bool
    98  	// An optional function that allows you to specify other calls
    99  	wantFunc func(t *testing.T)
   100  }
   101  
   102  // CLITestSuite is the CLI integration tests.
   103  type CLITestSuite struct {
   104  	suite.Suite
   105  }
   106  
   107  // TestCLI runs the CLI integration test suite.
   108  func TestCLI(t *testing.T) {
   109  	suite.Run(t, new(CLITestSuite))
   110  }
   111  
   112  func init() {
   113  	collectCoverage := os.Getenv("INTEG_COVER")
   114  	cover = collectCoverage == "on"
   115  	ciEnv := os.Getenv("CI")
   116  	if ciEnv == "on" {
   117  		ccloudTestBin = ccloudTestBinRace
   118  		confluentTestBin = confluentTestBinRace
   119  	}
   120  	if runtime.GOOS == "windows" {
   121  		ccloudTestBin = ccloudTestBin + ".exe"
   122  		confluentTestBin = confluentTestBin + ".exe"
   123  	}
   124  }
   125  
   126  // SetupSuite builds the CLI binary to test
   127  func (s *CLITestSuite) SetupSuite() {
   128  	covCollector = bincover.NewCoverageCollector(mergedCoverageFilename, cover)
   129  	covCollector.Setup()
   130  	req := require.New(s.T())
   131  
   132  	// dumb but effective
   133  	err := os.Chdir("..")
   134  	req.NoError(err)
   135  	err = os.Setenv("XX_CCLOUD_RBAC", "yes")
   136  	req.NoError(err)
   137  	for _, binary := range []string{ccloudTestBin, confluentTestBin} {
   138  		if _, err = os.Stat(binaryPath(s.T(), binary)); os.IsNotExist(err) || !*noRebuild {
   139  			var makeArgs string
   140  			if ccloudTestBin == ccloudTestBinRace {
   141  				makeArgs = "build-integ-race"
   142  			} else {
   143  				makeArgs = "build-integ-nonrace"
   144  			}
   145  			makeCmd := exec.Command("make", makeArgs)
   146  			output, err := makeCmd.CombinedOutput()
   147  			if err != nil {
   148  				s.T().Log(string(output))
   149  				req.NoError(err)
   150  			}
   151  		}
   152  	}
   153  }
   154  
   155  func (s *CLITestSuite) TearDownSuite() {
   156  	// Merge coverage profiles.
   157  	_ = os.Unsetenv("XX_CCLOUD_RBAC")
   158  	covCollector.TearDown()
   159  }
   160  
   161  func (s *CLITestSuite) TestConfluentHelp() {
   162  	var tests []CLITest
   163  	if runtime.GOOS == "windows" {
   164  		tests = []CLITest{
   165  			{name: "no args", fixture: "confluent-help-flag-windows.golden", wantErrCode: 1},
   166  			{args: "help", fixture: "confluent-help-windows.golden"},
   167  			{args: "--help", fixture: "confluent-help-flag-windows.golden"},
   168  			{args: "version", fixture: "confluent-version.golden", regex: true},
   169  		}
   170  	} else {
   171  		tests = []CLITest{
   172  			{name: "no args", fixture: "confluent-help-flag.golden", wantErrCode: 1},
   173  			{args: "help", fixture: "confluent-help.golden"},
   174  			{args: "--help", fixture: "confluent-help-flag.golden"},
   175  			{args: "version", fixture: "confluent-version.golden", regex: true},
   176  		}
   177  	}
   178  
   179  	loginURL := serveMds(s.T()).URL
   180  
   181  	for _, tt := range tests {
   182  		s.runConfluentTest(tt, loginURL)
   183  	}
   184  }
   185  
   186  func (s *CLITestSuite) TestCcloudHelp() {
   187  	tests := []CLITest{
   188  		{name: "no args", fixture: "help-flag-fail.golden", wantErrCode: 1},
   189  		{args: "help", fixture: "help.golden"},
   190  		{args: "--help", fixture: "help-flag.golden"},
   191  		{args: "version", fixture: "version.golden", regex: true},
   192  	}
   193  
   194  	kafkaURL := serveKafkaAPI(s.T()).URL
   195  	loginURL := serve(s.T(), kafkaURL).URL
   196  
   197  	for _, tt := range tests {
   198  		s.runCcloudTest(tt, loginURL)
   199  	}
   200  }
   201  
   202  func assertUserAgent(t *testing.T, expected string) func(w http.ResponseWriter, r *http.Request) {
   203  	return func(w http.ResponseWriter, r *http.Request) {
   204  		require.Regexp(t, expected, r.Header.Get("User-Agent"))
   205  	}
   206  }
   207  
   208  func (s *CLITestSuite) TestUserAgent() {
   209  	checkUserAgent := func(t *testing.T, expected string) string {
   210  		kafkaApiRouter := http.NewServeMux()
   211  		kafkaApiRouter.HandleFunc("/", assertUserAgent(t, expected))
   212  		kafkaApiServer := httptest.NewServer(kafkaApiRouter)
   213  		cloudRouter := http.NewServeMux()
   214  		cloudRouter.HandleFunc("/api/sessions", compose(assertUserAgent(t, expected), handleLogin(t)))
   215  		cloudRouter.HandleFunc("/api/me", compose(assertUserAgent(t, expected), handleMe(t)))
   216  		cloudRouter.HandleFunc("/api/check_email/", compose(assertUserAgent(t, expected), handleCheckEmail(t)))
   217  		cloudRouter.HandleFunc("/api/clusters/", compose(assertUserAgent(t, expected), handleKafkaClusterGetListDeleteDescribe(t, kafkaApiServer.URL)))
   218  		return httptest.NewServer(cloudRouter).URL
   219  	}
   220  
   221  	serverURL := checkUserAgent(s.T(), fmt.Sprintf("Confluent-Cloud-CLI/v(?:[0-9]\\.?){3}([^ ]*) \\(https://confluent.cloud; support@confluent.io\\) "+
   222  		"ccloud-sdk-go/%s \\(%s/%s; go[^ ]*\\)", ccloud.SDKVersion, runtime.GOOS, runtime.GOARCH))
   223  	env := []string{"XX_CCLOUD_EMAIL=valid@user.com", "XX_CCLOUD_PASSWORD=pass1"}
   224  
   225  	s.T().Run("ccloud login", func(tt *testing.T) {
   226  		_ = runCommand(tt, ccloudTestBin, env, "login --url "+serverURL, 0)
   227  	})
   228  	s.T().Run("ccloud cluster list", func(tt *testing.T) {
   229  		_ = runCommand(tt, ccloudTestBin, env, "kafka cluster list", 0)
   230  	})
   231  	s.T().Run("ccloud topic list", func(tt *testing.T) {
   232  		_ = runCommand(tt, ccloudTestBin, env, "kafka topic list --cluster lkc-abc123", 0)
   233  	})
   234  }
   235  
   236  func (s *CLITestSuite) TestCcloudErrors() {
   237  	type errorer interface {
   238  		GetError() *corev1.Error
   239  	}
   240  	serveErrors := func(t *testing.T) string {
   241  		req := require.New(t)
   242  		write := func(w http.ResponseWriter, resp proto.Message) {
   243  			if r, ok := resp.(errorer); ok {
   244  				w.WriteHeader(int(r.GetError().Code))
   245  			}
   246  			b, err := utilv1.MarshalJSONToBytes(resp)
   247  			req.NoError(err)
   248  			_, err = io.WriteString(w, string(b))
   249  			req.NoError(err)
   250  		}
   251  		router := http.NewServeMux()
   252  		router.HandleFunc("/api/sessions", handleLogin(t))
   253  		router.HandleFunc("/api/me", handleMe(t))
   254  		router.HandleFunc("/api/check_email/", handleCheckEmail(t))
   255  		router.HandleFunc("/api/clusters", func(w http.ResponseWriter, r *http.Request) {
   256  			switch r.Header.Get("Authorization") {
   257  			// TODO: these assume the upstream doesn't change its error responses. Fragile, fragile, fragile. :(
   258  			// https://github.com/confluentinc/cc-auth-service/blob/06db0bebb13fb64c9bc3c6e2cf0b67709b966632/jwt/token.go#L23
   259  			case "Bearer expired":
   260  				write(w, &schedv1.GetKafkaClustersReply{Error: &corev1.Error{Message: "token is expired", Code: http.StatusUnauthorized}})
   261  			case "Bearer malformed":
   262  				write(w, &schedv1.GetKafkaClustersReply{Error: &corev1.Error{Message: "malformed token", Code: http.StatusBadRequest}})
   263  			case "Bearer invalid":
   264  				// TODO: The response for an invalid token should be 4xx, not 500 (e.g., if you take a working token from devel and try in stag)
   265  				write(w, &schedv1.GetKafkaClustersReply{Error: &corev1.Error{Message: "Token parsing error: crypto/rsa: verification error", Code: http.StatusInternalServerError}})
   266  			default:
   267  				req.Fail("reached the unreachable", "auth=%s", r.Header.Get("Authorization"))
   268  			}
   269  		})
   270  		server := httptest.NewServer(router)
   271  		return server.URL
   272  	}
   273  
   274  	s.T().Run("invalid user or pass", func(tt *testing.T) {
   275  		loginURL := serveErrors(tt)
   276  		env := []string{"XX_CCLOUD_EMAIL=incorrect@user.com", "XX_CCLOUD_PASSWORD=pass1"}
   277  		output := runCommand(tt, ccloudTestBin, env, "login --url "+loginURL, 1)
   278  		require.Contains(tt, output, errors.InvalidLoginErrorMsg)
   279  		require.Contains(tt, output, errors.ComposeSuggestionsMessage(errors.CCloudInvalidLoginSuggestions))
   280  	})
   281  
   282  	s.T().Run("expired token", func(tt *testing.T) {
   283  		loginURL := serveErrors(tt)
   284  		env := []string{"XX_CCLOUD_EMAIL=expired@user.com", "XX_CCLOUD_PASSWORD=pass1"}
   285  		output := runCommand(tt, ccloudTestBin, env, "login --url "+loginURL, 0)
   286  		require.Contains(tt, output, fmt.Sprintf(errors.LoggedInAsMsg, "expired@user.com"))
   287  		require.Contains(tt, output, fmt.Sprintf(errors.LoggedInUsingEnvMsg, "a-595", "default"))
   288  		output = runCommand(tt, ccloudTestBin, []string{}, "kafka cluster list", 1)
   289  		require.Contains(tt, output, errors.TokenExpiredMsg)
   290  		require.Contains(tt, output, errors.NotLoggedInErrorMsg)
   291  	})
   292  
   293  	s.T().Run("malformed token", func(tt *testing.T) {
   294  		loginURL := serveErrors(tt)
   295  		env := []string{"XX_CCLOUD_EMAIL=malformed@user.com", "XX_CCLOUD_PASSWORD=pass1"}
   296  		output := runCommand(tt, ccloudTestBin, env, "login --url "+loginURL, 0)
   297  		require.Contains(tt, output, fmt.Sprintf(errors.LoggedInAsMsg, "malformed@user.com"))
   298  		require.Contains(tt, output, fmt.Sprintf(errors.LoggedInUsingEnvMsg, "a-595", "default"))
   299  
   300  		output = runCommand(s.T(), ccloudTestBin, []string{}, "kafka cluster list", 1)
   301  		require.Contains(tt, output, errors.CorruptedTokenErrorMsg)
   302  		require.Contains(tt, output, errors.ComposeSuggestionsMessage(errors.CorruptedTokenSuggestions))
   303  	})
   304  
   305  	s.T().Run("invalid jwt", func(tt *testing.T) {
   306  		loginURL := serveErrors(tt)
   307  		env := []string{"XX_CCLOUD_EMAIL=invalid@user.com", "XX_CCLOUD_PASSWORD=pass1"}
   308  		output := runCommand(tt, ccloudTestBin, env, "login --url "+loginURL, 0)
   309  		require.Contains(tt, output, fmt.Sprintf(errors.LoggedInAsMsg, "invalid@user.com"))
   310  		require.Contains(tt, output, fmt.Sprintf(errors.LoggedInUsingEnvMsg, "a-595", "default"))
   311  
   312  		output = runCommand(s.T(), ccloudTestBin, []string{}, "kafka cluster list", 1)
   313  		require.Contains(tt, output, errors.CorruptedTokenErrorMsg)
   314  		require.Contains(tt, output, errors.ComposeSuggestionsMessage(errors.CorruptedTokenSuggestions))
   315  	})
   316  }
   317  
   318  func (s *CLITestSuite) runCcloudTest(tt CLITest, loginURL string) {
   319  	if tt.name == "" {
   320  		tt.name = tt.args
   321  	}
   322  	if strings.HasPrefix(tt.name, "error") {
   323  		tt.wantErrCode = 1
   324  	}
   325  
   326  	s.T().Run(tt.name, func(t *testing.T) {
   327  		if !tt.workflow {
   328  			resetConfiguration(t, "ccloud")
   329  		}
   330  
   331  		if tt.login == "default" {
   332  			env := []string{"XX_CCLOUD_EMAIL=fake@user.com", "XX_CCLOUD_PASSWORD=pass1"}
   333  			output := runCommand(t, ccloudTestBin, env, "login --url "+loginURL, 0)
   334  			if *debug {
   335  				fmt.Println(output)
   336  			}
   337  		}
   338  
   339  		if tt.useKafka != "" {
   340  			output := runCommand(t, ccloudTestBin, []string{}, "kafka cluster use "+tt.useKafka, 0)
   341  			if *debug {
   342  				fmt.Println(output)
   343  			}
   344  		}
   345  
   346  		if tt.authKafka != "" {
   347  			output := runCommand(t, ccloudTestBin, []string{}, "api-key create --resource "+tt.useKafka, 0)
   348  			if *debug {
   349  				fmt.Println(output)
   350  			}
   351  			// HACK: we don't have scriptable output yet so we parse it from the table
   352  			key := strings.TrimSpace(strings.Split(strings.Split(output, "\n")[3], "|")[2])
   353  			output = runCommand(t, ccloudTestBin, []string{}, fmt.Sprintf("api-key use %s --resource %s", key, tt.useKafka), 0)
   354  			if *debug {
   355  				fmt.Println(output)
   356  			}
   357  		}
   358  		output := runCommand(t, ccloudTestBin, tt.env, tt.args, tt.wantErrCode)
   359  		if *debug {
   360  			fmt.Println(output)
   361  		}
   362  
   363  		if strings.HasPrefix(tt.args, "kafka cluster create") ||
   364  			strings.HasPrefix(tt.args, "config context current") {
   365  			re := regexp.MustCompile("https?://127.0.0.1:[0-9]+")
   366  			output = re.ReplaceAllString(output, "http://127.0.0.1:12345")
   367  		}
   368  
   369  		s.validateTestOutput(tt, t, output)
   370  	})
   371  }
   372  
   373  func (s *CLITestSuite) runConfluentTest(tt CLITest, loginURL string) {
   374  	if tt.name == "" {
   375  		tt.name = tt.args
   376  	}
   377  	if strings.HasPrefix(tt.name, "error") {
   378  		tt.wantErrCode = 1
   379  	}
   380  	s.T().Run(tt.name, func(t *testing.T) {
   381  		if !tt.workflow {
   382  			resetConfiguration(t, "confluent")
   383  		}
   384  
   385  		if tt.login == "default" {
   386  			env := []string{"XX_CONFLUENT_USERNAME=fake@user.com", "XX_CONFLUENT_PASSWORD=pass1"}
   387  			output := runCommand(t, confluentTestBin, env, "login --url "+loginURL, 0)
   388  			if *debug {
   389  				fmt.Println(output)
   390  			}
   391  		}
   392  
   393  		output := runCommand(t, confluentTestBin, []string{}, tt.args, tt.wantErrCode)
   394  
   395  		if strings.HasPrefix(tt.args, "config context list") ||
   396  			strings.HasPrefix(tt.args, "config context current") {
   397  			re := regexp.MustCompile("https?://127.0.0.1:[0-9]+")
   398  			output = re.ReplaceAllString(output, "http://127.0.0.1:12345")
   399  		}
   400  
   401  		s.validateTestOutput(tt, t, output)
   402  	})
   403  }
   404  
   405  func (s *CLITestSuite) validateTestOutput(tt CLITest, t *testing.T, output string) {
   406  	if *update && !tt.regex && tt.fixture != "" {
   407  		writeFixture(t, tt.fixture, output)
   408  	}
   409  	actual := utils.NormalizeNewLines(output)
   410  	if tt.contains != "" {
   411  		require.Contains(t, actual, tt.contains)
   412  	} else if tt.notContains != "" {
   413  		require.NotContains(t, actual, tt.notContains)
   414  	} else if tt.fixture != "" {
   415  		expected := utils.NormalizeNewLines(LoadFixture(t, tt.fixture))
   416  		if tt.regex {
   417  			require.Regexp(t, expected, actual)
   418  		} else if !reflect.DeepEqual(actual, expected) {
   419  			t.Fatalf("\n   actual:\n%s\nexpected:\n%s", actual, expected)
   420  		}
   421  	}
   422  	if tt.wantFunc != nil {
   423  		tt.wantFunc(t)
   424  	}
   425  }
   426  
   427  func runCommand(t *testing.T, binaryName string, env []string, args string, wantErrCode int) string {
   428  	output, exitCode, err := covCollector.RunBinary(binaryPath(t, binaryName), "TestRunMain", env, strings.Split(args, " "))
   429  	if err != nil && wantErrCode == 0 {
   430  		require.Failf(t, "unexpected error",
   431  			"exit %d: %s\n%s", exitCode, args, output)
   432  	}
   433  	require.Equal(t, wantErrCode, exitCode, output)
   434  	return output
   435  }
   436  
   437  func resetConfiguration(t *testing.T, cliName string) {
   438  	// HACK: delete your current config to isolate tests cases for non-workflow tests...
   439  	// probably don't really want to do this or devs will get mad
   440  	cfg := v3.New(&config.Params{
   441  		CLIName: cliName,
   442  	})
   443  	err := cfg.Save()
   444  	require.NoError(t, err)
   445  }
   446  
   447  func writeFixture(t *testing.T, fixture string, content string) {
   448  	err := ioutil.WriteFile(FixturePath(t, fixture), []byte(content), 0644)
   449  	if err != nil {
   450  		t.Fatal(err)
   451  	}
   452  }
   453  
   454  func binaryPath(t *testing.T, binaryName string) string {
   455  	dir, err := os.Getwd()
   456  	require.NoError(t, err)
   457  	return path.Join(dir, binaryName)
   458  }
   459  
   460  var keyStore = map[int32]*schedv1.ApiKey{}
   461  var keyIndex = int32(1)
   462  
   463  type ApiKeyList []*schedv1.ApiKey
   464  
   465  // Len is part of sort.Interface.
   466  func (d ApiKeyList) Len() int {
   467  	return len(d)
   468  }
   469  
   470  // Swap is part of sort.Interface.
   471  func (d ApiKeyList) Swap(i, j int) {
   472  	d[i], d[j] = d[j], d[i]
   473  }
   474  
   475  // Less is part of sort.Interface. We use Key as the value to sort by
   476  func (d ApiKeyList) Less(i, j int) bool {
   477  	return d[i].Key < d[j].Key
   478  }
   479  
   480  func init() {
   481  	keyStore[keyIndex] = &schedv1.ApiKey{
   482  		Id:     keyIndex,
   483  		Key:    "MYKEY1",
   484  		Secret: "MYSECRET1",
   485  		LogicalClusters: []*schedv1.ApiKey_Cluster{
   486  			{Id: "lkc-bob", Type: "kafka"},
   487  		},
   488  		UserId: 12,
   489  	}
   490  	keyIndex += 1
   491  	keyStore[keyIndex] = &schedv1.ApiKey{
   492  		Id:     keyIndex,
   493  		Key:    "MYKEY2",
   494  		Secret: "MYSECRET2",
   495  		LogicalClusters: []*schedv1.ApiKey_Cluster{
   496  			{Id: "lkc-abc", Type: "kafka"},
   497  		},
   498  		UserId: 18,
   499  	}
   500  	keyIndex += 1
   501  	keyStore[100] = &schedv1.ApiKey{
   502  		Id:     keyIndex,
   503  		Key:    "UIAPIKEY100",
   504  		Secret: "UIAPISECRET100",
   505  		LogicalClusters: []*schedv1.ApiKey_Cluster{
   506  			{Id: "lkc-cool1", Type: "kafka"},
   507  		},
   508  		UserId: 25,
   509  	}
   510  	keyStore[101] = &schedv1.ApiKey{
   511  		Id:     keyIndex,
   512  		Key:    "UIAPIKEY101",
   513  		Secret: "UIAPISECRET101",
   514  		LogicalClusters: []*schedv1.ApiKey_Cluster{
   515  			{Id: "lkc-other1", Type: "kafka"},
   516  		},
   517  		UserId: 25,
   518  	}
   519  	keyStore[102] = &schedv1.ApiKey{
   520  		Id:     keyIndex,
   521  		Key:    "UIAPIKEY102",
   522  		Secret: "UIAPISECRET102",
   523  		LogicalClusters: []*schedv1.ApiKey_Cluster{
   524  			{Id: "lksqlc-ksql1", Type: "ksql"},
   525  		},
   526  		UserId: 25,
   527  	}
   528  	keyStore[103] = &schedv1.ApiKey{
   529  		Id:     keyIndex,
   530  		Key:    "UIAPIKEY103",
   531  		Secret: "UIAPISECRET103",
   532  		LogicalClusters: []*schedv1.ApiKey_Cluster{
   533  			{Id: "lkc-cool1", Type: "kafka"},
   534  		},
   535  		UserId: 25,
   536  	}
   537  }
   538  
   539  func serve(t *testing.T, kafkaAPIURL string) *httptest.Server {
   540  	router := http.NewServeMux()
   541  	router.HandleFunc("/api/sessions", handleLogin(t))
   542  	router.HandleFunc("/api/check_email/", handleCheckEmail(t))
   543  	router.HandleFunc("/api/me", handleMe(t))
   544  	router.HandleFunc("/api/api_keys", func(w http.ResponseWriter, r *http.Request) {
   545  		if r.Method == "POST" {
   546  			req := &schedv1.CreateApiKeyRequest{}
   547  			err := utilv1.UnmarshalJSON(r.Body, req)
   548  			require.NoError(t, err)
   549  			require.NotEmpty(t, req.ApiKey.AccountId)
   550  			apiKey := req.ApiKey
   551  			apiKey.Id = keyIndex
   552  			apiKey.Key = fmt.Sprintf("MYKEY%d", keyIndex)
   553  			apiKey.Secret = fmt.Sprintf("MYSECRET%d", keyIndex)
   554  			if req.ApiKey.UserId == 0 {
   555  				apiKey.UserId = 23
   556  			} else {
   557  				apiKey.UserId = req.ApiKey.UserId
   558  			}
   559  			keyIndex++
   560  			keyStore[apiKey.Id] = apiKey
   561  			b, err := utilv1.MarshalJSONToBytes(&schedv1.CreateApiKeyReply{ApiKey: apiKey})
   562  			require.NoError(t, err)
   563  			_, err = io.WriteString(w, string(b))
   564  			require.NoError(t, err)
   565  		} else if r.Method == "GET" {
   566  			require.NotEmpty(t, r.URL.Query().Get("account_id"))
   567  			apiKeys := apiKeysFilter(r.URL)
   568  			// Return sorted data or the test output will not be stable
   569  			sort.Sort(ApiKeyList(apiKeys))
   570  			b, err := utilv1.MarshalJSONToBytes(&schedv1.GetApiKeysReply{ApiKeys: apiKeys})
   571  			require.NoError(t, err)
   572  			_, err = io.WriteString(w, string(b))
   573  			require.NoError(t, err)
   574  		}
   575  	})
   576  	router.HandleFunc("/api/api_keys/", handleAPIKeyUpdateAndDelete(t))
   577  	router.HandleFunc("/api/accounts", func(w http.ResponseWriter, r *http.Request) {
   578  		if r.Method == "GET" {
   579  			b, err := utilv1.MarshalJSONToBytes(&orgv1.ListAccountsReply{Accounts: environments})
   580  			require.NoError(t, err)
   581  			_, err = io.WriteString(w, string(b))
   582  			require.NoError(t, err)
   583  		} else if r.Method == "POST" {
   584  			req := &orgv1.CreateAccountRequest{}
   585  			err := utilv1.UnmarshalJSON(r.Body, req)
   586  			require.NoError(t, err)
   587  			account := &orgv1.Account{
   588  				Id:             "a-5555",
   589  				Name:           req.Account.Name,
   590  				OrganizationId: 0,
   591  			}
   592  			b, err := utilv1.MarshalJSONToBytes(&orgv1.CreateAccountReply{
   593  				Account: account,
   594  			})
   595  			require.NoError(t, err)
   596  			_, err = io.WriteString(w, string(b))
   597  			require.NoError(t, err)
   598  		}
   599  	})
   600  	router.HandleFunc("/api/accounts/a-595", handleEnvironmentRequests(t, "a-595"))
   601  	router.HandleFunc("/api/accounts/not-595", handleEnvironmentRequests(t, "not-595"))
   602  	router.HandleFunc("/api/clusters/lkc-describe", handleKafkaClusterDescribeTest(t))
   603  	router.HandleFunc("/api/clusters/lkc-describe-dedicated", handleKafkaClusterDescribeTest(t))
   604  	router.HandleFunc("/api/clusters/lkc-describe-dedicated-pending", handleKafkaClusterDescribeTest(t))
   605  	router.HandleFunc("/api/clusters/lkc-describe-dedicated-with-encryption", handleKafkaClusterDescribeTest(t))
   606  	router.HandleFunc("/api/clusters/lkc-update", handleKafkaClusterUpdateTest(t))
   607  	router.HandleFunc("/api/clusters/lkc-update-dedicated", handleKafkaDedicatedClusterUpdateTest(t))
   608  	router.HandleFunc("/api/clusters/", handleKafkaClusterGetListDeleteDescribe(t, kafkaAPIURL))
   609  	router.HandleFunc("/api/clusters", func(w http.ResponseWriter, r *http.Request) {
   610  		if r.Method == "POST" {
   611  			handleKafkaClusterCreate(t, kafkaAPIURL)(w, r)
   612  		} else if r.Method == "GET" {
   613  			cluster := schedv1.KafkaCluster{
   614  				Id:              "lkc-123",
   615  				Name:            "abc",
   616  				Deployment:      &schedv1.Deployment{Sku: productv1.Sku_BASIC},
   617  				Durability:      0,
   618  				Status:          0,
   619  				Region:          "us-central1",
   620  				ServiceProvider: "gcp",
   621  			}
   622  			clusterMultizone := schedv1.KafkaCluster{
   623  				Id:              "lkc-456",
   624  				Name:            "def",
   625  				Deployment:      &schedv1.Deployment{Sku: productv1.Sku_BASIC},
   626  				Durability:      1,
   627  				Status:          0,
   628  				Region:          "us-central1",
   629  				ServiceProvider: "gcp",
   630  			}
   631  			b, err := utilv1.MarshalJSONToBytes(&schedv1.GetKafkaClustersReply{
   632  				Clusters: []*schedv1.KafkaCluster{&cluster, &clusterMultizone},
   633  			})
   634  			require.NoError(t, err)
   635  			_, err = io.WriteString(w, string(b))
   636  			require.NoError(t, err)
   637  		}
   638  	})
   639  	router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   640  		_, err := io.WriteString(w, `{"error": {"message": "unexpected call to `+r.URL.Path+`"}}`)
   641  		require.NoError(t, err)
   642  	})
   643  	router.HandleFunc("/api/schema_registries/", func(w http.ResponseWriter, r *http.Request) {
   644  		q := r.URL.Query()
   645  		id := q.Get("id")
   646  		if id == "" {
   647  			id = "lsrc-1234"
   648  		}
   649  		accountId := q.Get("account_id")
   650  		srCluster := &schedv1.SchemaRegistryCluster{
   651  			Id:        id,
   652  			AccountId: accountId,
   653  			Name:      "account schema-registry",
   654  			Endpoint:  "SASL_SSL://sr-endpoint",
   655  		}
   656  		fmt.Println(srCluster)
   657  		b, err := utilv1.MarshalJSONToBytes(&schedv1.GetSchemaRegistryClusterReply{
   658  			Cluster: srCluster,
   659  		})
   660  		require.NoError(t, err)
   661  		_, err = io.WriteString(w, string(b))
   662  		require.NoError(t, err)
   663  	})
   664  	router.HandleFunc("/api/service_accounts", handleServiceAccountRequests(t))
   665  	router.HandleFunc("/api/accounts/a-595/clusters/lkc-123/connectors/az-connector/", func(w http.ResponseWriter, r *http.Request) {
   666  		w.WriteHeader(http.StatusNoContent)
   667  		return
   668  	})
   669  	router.HandleFunc("/api/accounts/a-595/clusters/lkc-123/connectors", handleConnect(t))
   670  	router.HandleFunc("/api/accounts/a-595/clusters/lkc-123/connector-plugins/GcsSink/config/validate", handleConnectorCatalogDescribe(t))
   671  	router.HandleFunc("/api/accounts/a-595/clusters/lkc-123/connector-plugins", handleConnectPlugins(t))
   672  	router.HandleFunc("/api/ksqls", handleKSQLCreateList(t))
   673  	router.HandleFunc("/api/ksqls/lksqlc-ksql1/", func(w http.ResponseWriter, r *http.Request) {
   674  		ksqlCluster := &schedv1.KSQLCluster{
   675  			Id:                "lksqlc-ksql1",
   676  			AccountId:         "25",
   677  			KafkaClusterId:    "lkc-12345",
   678  			OutputTopicPrefix: "pksqlc-abcde",
   679  			Name:              "account ksql",
   680  			Storage:           101,
   681  			Endpoint:          "SASL_SSL://ksql-endpoint",
   682  		}
   683  		reply, err := utilv1.MarshalJSONToBytes(&schedv1.GetKSQLClusterReply{
   684  			Cluster: ksqlCluster,
   685  		})
   686  		require.NoError(t, err)
   687  		_, err = io.WriteString(w, string(reply))
   688  		require.NoError(t, err)
   689  	})
   690  	router.HandleFunc("/api/ksqls/lksqlc-12345", func(w http.ResponseWriter, r *http.Request) {
   691  		ksqlCluster := &schedv1.KSQLCluster{
   692  			Id:                "lksqlc-12345",
   693  			AccountId:         "25",
   694  			KafkaClusterId:    "lkc-abcde",
   695  			OutputTopicPrefix: "pksqlc-zxcvb",
   696  			Name:              "account ksql",
   697  			Storage:           130,
   698  			Endpoint:          "SASL_SSL://ksql-endpoint",
   699  		}
   700  		reply, err := utilv1.MarshalJSONToBytes(&schedv1.GetKSQLClusterReply{
   701  			Cluster: ksqlCluster,
   702  		})
   703  		require.NoError(t, err)
   704  		_, err = io.WriteString(w, string(reply))
   705  		require.NoError(t, err)
   706  	})
   707  	router.HandleFunc("/api/env_metadata", func(w http.ResponseWriter, r *http.Request) {
   708  		clouds := []*schedv1.CloudMetadata{
   709  			{
   710  				Id:   "gcp",
   711  				Name: "Google Cloud Platform",
   712  				Regions: []*schedv1.Region{
   713  					{
   714  						Id:            "asia-southeast1",
   715  						Name:          "asia-southeast1 (Singapore)",
   716  						IsSchedulable: true,
   717  					},
   718  					{
   719  						Id:            "asia-east2",
   720  						Name:          "asia-east2 (Hong Kong)",
   721  						IsSchedulable: true,
   722  					},
   723  				},
   724  			},
   725  			{
   726  				Id:   "aws",
   727  				Name: "Amazon Web Services",
   728  				Regions: []*schedv1.Region{
   729  					{
   730  						Id:            "ap-northeast-1",
   731  						Name:          "ap-northeast-1 (Tokyo)",
   732  						IsSchedulable: false,
   733  					},
   734  					{
   735  						Id:            "us-east-1",
   736  						Name:          "us-east-1 (N. Virginia)",
   737  						IsSchedulable: true,
   738  					},
   739  				},
   740  			},
   741  			{
   742  				Id:   "azure",
   743  				Name: "Azure",
   744  				Regions: []*schedv1.Region{
   745  					{
   746  						Id:            "southeastasia",
   747  						Name:          "southeastasia (Singapore)",
   748  						IsSchedulable: false,
   749  					},
   750  				},
   751  			},
   752  		}
   753  		reply, err := utilv1.MarshalJSONToBytes(&schedv1.GetEnvironmentMetadataReply{
   754  			Clouds: clouds,
   755  		})
   756  		require.NoError(t, err)
   757  		_, err = io.WriteString(w, string(reply))
   758  		require.NoError(t, err)
   759  	})
   760  	router.HandleFunc("/api/organizations/0/price_table", handlePriceTable(t))
   761  	addMdsv2alpha1(t, router)
   762  	return httptest.NewServer(router)
   763  }
   764  
   765  func apiKeysFilter(url *url.URL) []*schedv1.ApiKey {
   766  	var apiKeys []*schedv1.ApiKey
   767  	q := url.Query()
   768  	uid := q.Get("user_id")
   769  	clusterIds := q["cluster_id"]
   770  
   771  	for _, a := range keyStore {
   772  		uidFilter := (uid == "0") || (uid == strconv.Itoa(int(a.UserId)))
   773  		clusterFilter := (len(clusterIds) == 0) || func(clusterIds []string) bool {
   774  			for _, c := range a.LogicalClusters {
   775  				for _, clusterId := range clusterIds {
   776  					if c.Id == clusterId {
   777  						return true
   778  					}
   779  				}
   780  			}
   781  			return false
   782  		}(clusterIds)
   783  
   784  		if uidFilter && clusterFilter {
   785  			apiKeys = append(apiKeys, a)
   786  		}
   787  	}
   788  	return apiKeys
   789  }
   790  
   791  func serveKafkaAPI(t *testing.T) *httptest.Server {
   792  	mux := http.NewServeMux()
   793  	mux.HandleFunc("/2.0/kafka/lkc-acls/acls:search", handleKafkaACLsList(t))
   794  	mux.HandleFunc("/2.0/kafka/lkc-acls/acls", handleKafkaACLsCreate(t))
   795  	mux.HandleFunc("/2.0/kafka/lkc-acls/acls/delete", handleKafkaACLsDelete(t))
   796  
   797  	mux.HandleFunc("/2.0/kafka/lkc-links/links/", handleKafkaLinks(t))
   798  
   799  	mux.HandleFunc("/2.0/kafka/lkc-topics/topics/test-topic/mirror:stop", func(w http.ResponseWriter, r *http.Request) {
   800  		if r.Method == "POST" {
   801  			w.WriteHeader(http.StatusNoContent)
   802  		}
   803  	})
   804  	mux.HandleFunc("/2.0/kafka/lkc-topics/topics/not-found/mirror:stop", func(w http.ResponseWriter, r *http.Request) {
   805  		if r.Method == "POST" {
   806  			w.WriteHeader(http.StatusNotFound)
   807  		}
   808  	})
   809  
   810  	// TODO: no idea how this "topic already exists" API request or response actually looks
   811  	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   812  		w.WriteHeader(400)
   813  		_, err := io.WriteString(w, `{}`)
   814  		require.NoError(t, err)
   815  	})
   816  	return httptest.NewServer(mux)
   817  }
   818  
   819  func handleKafkaLinks(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
   820  	return func(w http.ResponseWriter, r *http.Request) {
   821  		parts := strings.Split(r.URL.Path, "/")
   822  		lastElem := parts[len(parts)-1]
   823  
   824  		if lastElem == "" {
   825  			// No specific link here, we want a list of ALL links
   826  
   827  			listResponsePayload := []*linkv1.ListLinksResponseItem{
   828  				&linkv1.ListLinksResponseItem{LinkName: "link-1", LinkId: "1234", ClusterId: "Blah"},
   829  				&linkv1.ListLinksResponseItem{LinkName: "link-2", LinkId: "4567", ClusterId: "blah"},
   830  			}
   831  
   832  			listReply, err := json.Marshal(listResponsePayload)
   833  			require.NoError(t, err)
   834  			_, err = io.WriteString(w, string(listReply))
   835  			require.NoError(t, err)
   836  		} else {
   837  			// Return properties for the selected link.
   838  
   839  			describeResponsePayload := linkv1.DescribeLinkResponse{
   840  				Entries: []*linkv1.DescribeLinkResponseEntry{
   841  					{
   842  						Name:  "replica.fetch.max.bytes",
   843  						Value: "1048576",
   844  					},
   845  				},
   846  			}
   847  			describeReply, err := json.Marshal(describeResponsePayload)
   848  			require.NoError(t, err)
   849  			_, err = io.WriteString(w, string(describeReply))
   850  			require.NoError(t, err)
   851  		}
   852  	}
   853  }
   854  
   855  func handleLogin(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
   856  	return func(w http.ResponseWriter, r *http.Request) {
   857  		req := require.New(t)
   858  		b, err := ioutil.ReadAll(r.Body)
   859  		req.NoError(err)
   860  		auth := &struct {
   861  			Email    string
   862  			Password string
   863  		}{}
   864  		err = json.Unmarshal(b, auth)
   865  		req.NoError(err)
   866  		switch auth.Email {
   867  		case "incorrect@user.com":
   868  			w.WriteHeader(http.StatusForbidden)
   869  		case "expired@user.com":
   870  			http.SetCookie(w, &http.Cookie{Name: "auth_token", Value: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE1MzAxMjQ4NTcsImV4cCI6MTUzMDAzODQ1NywiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSJ9.Y2ui08GPxxuV9edXUBq-JKr1VPpMSnhjSFySczCby7Y"})
   871  		case "malformed@user.com":
   872  			http.SetCookie(w, &http.Cookie{Name: "auth_token", Value: "malformed"})
   873  		case "invalid@user.com":
   874  			http.SetCookie(w, &http.Cookie{Name: "auth_token", Value: "invalid"})
   875  		default:
   876  			http.SetCookie(w, &http.Cookie{Name: "auth_token", Value: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE1NjE2NjA4NTcsImV4cCI6MjUzMzg2MDM4NDU3LCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIn0.G6IgrFm5i0mN7Lz9tkZQ2tZvuZ2U7HKnvxMuZAooPmE"})
   877  		}
   878  	}
   879  }
   880  
   881  func handleMe(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
   882  	return func(w http.ResponseWriter, r *http.Request) {
   883  		b, err := utilv1.MarshalJSONToBytes(&orgv1.GetUserReply{
   884  			User: &orgv1.User{
   885  				Id:         23,
   886  				Email:      "cody@confluent.io",
   887  				FirstName:  "Cody",
   888  				ResourceId: "u-11aaa",
   889  			},
   890  			Accounts: environments,
   891  		})
   892  		require.NoError(t, err)
   893  		_, err = io.WriteString(w, string(b))
   894  		require.NoError(t, err)
   895  	}
   896  }
   897  
   898  func handleCheckEmail(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
   899  	return func(w http.ResponseWriter, r *http.Request) {
   900  		req := require.New(t)
   901  		email := strings.Replace(r.URL.String(), "/api/check_email/", "", 1)
   902  		reply := &orgv1.GetUserReply{}
   903  		switch email {
   904  		case "cody@confluent.io":
   905  			reply.User = &orgv1.User{
   906  				Email: "cody@confluent.io",
   907  			}
   908  		}
   909  		b, err := utilv1.MarshalJSONToBytes(reply)
   910  		req.NoError(err)
   911  		_, err = io.WriteString(w, string(b))
   912  		req.NoError(err)
   913  	}
   914  }
   915  
   916  func handleKafkaClusterGetListDeleteDescribe(t *testing.T, kafkaAPIURL string) func(w http.ResponseWriter, r *http.Request) {
   917  	return func(w http.ResponseWriter, r *http.Request) {
   918  		parts := strings.Split(r.URL.Path, "/")
   919  		id := parts[len(parts)-1]
   920  		if id == "lkc-unknown" {
   921  			_, err := io.WriteString(w, `{"error":{"code":404,"message":"resource not found","nested_errors":{},"details":[],"stack":null},"cluster":null}`)
   922  			require.NoError(t, err)
   923  			return
   924  		}
   925  		if r.Method == "DELETE" {
   926  			w.WriteHeader(http.StatusNoContent)
   927  			return
   928  		} else {
   929  			// this is in the body of delete requests
   930  			require.NotEmpty(t, r.URL.Query().Get("account_id"))
   931  		}
   932  		// Now return the KafkaCluster with updated ApiEndpoint
   933  		b, err := utilv1.MarshalJSONToBytes(&schedv1.GetKafkaClusterReply{
   934  			Cluster: &schedv1.KafkaCluster{
   935  				Id:              id,
   936  				Name:            "kafka-cluster",
   937  				Deployment:      &schedv1.Deployment{Sku: productv1.Sku_BASIC},
   938  				NetworkIngress:  100,
   939  				NetworkEgress:   100,
   940  				Storage:         500,
   941  				ServiceProvider: "aws",
   942  				Region:          "us-west-2",
   943  				Endpoint:        "SASL_SSL://kafka-endpoint",
   944  				ApiEndpoint:     kafkaAPIURL,
   945  			},
   946  		})
   947  		require.NoError(t, err)
   948  		_, err = io.WriteString(w, string(b))
   949  		require.NoError(t, err)
   950  	}
   951  }
   952  
   953  func handleKafkaClusterDescribeTest(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
   954  	return func(w http.ResponseWriter, r *http.Request) {
   955  		id := r.URL.Query().Get("id")
   956  		cluster := &schedv1.KafkaCluster{
   957  			Id:              id,
   958  			Name:            "kafka-cluster",
   959  			Deployment:      &schedv1.Deployment{Sku: productv1.Sku_BASIC},
   960  			NetworkIngress:  100,
   961  			NetworkEgress:   100,
   962  			Storage:         500,
   963  			ServiceProvider: "aws",
   964  			Region:          "us-west-2",
   965  			Endpoint:        "SASL_SSL://kafka-endpoint",
   966  			ApiEndpoint:     "http://kafka-api-url",
   967  		}
   968  		switch id {
   969  		case "lkc-describe-dedicated":
   970  			cluster.Cku = 1
   971  			cluster.Deployment = &schedv1.Deployment{Sku: productv1.Sku_DEDICATED}
   972  		case "lkc-describe-dedicated-pending":
   973  			cluster.Cku = 1
   974  			cluster.PendingCku = 2
   975  			cluster.Deployment = &schedv1.Deployment{Sku: productv1.Sku_DEDICATED}
   976  		case "lkc-describe-dedicated-with-encryption":
   977  			cluster.Cku = 1
   978  			cluster.EncryptionKeyId = "abc123"
   979  			cluster.Deployment = &schedv1.Deployment{Sku: productv1.Sku_DEDICATED}
   980  		}
   981  		b, err := utilv1.MarshalJSONToBytes(&schedv1.GetKafkaClusterReply{
   982  			Cluster: cluster,
   983  		})
   984  		require.NoError(t, err)
   985  		_, err = io.WriteString(w, string(b))
   986  		require.NoError(t, err)
   987  	}
   988  }
   989  
   990  func handleKafkaClusterUpdateTest(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
   991  	return func(w http.ResponseWriter, r *http.Request) {
   992  		// Describe client call
   993  		var out []byte
   994  		if r.Method == "GET" {
   995  			id := r.URL.Query().Get("id")
   996  			var err error
   997  			out, err = utilv1.MarshalJSONToBytes(&schedv1.GetKafkaClusterReply{
   998  				Cluster: &schedv1.KafkaCluster{
   999  					Id:              id,
  1000  					Name:            "lkc-update",
  1001  					Deployment:      &schedv1.Deployment{Sku: productv1.Sku_BASIC},
  1002  					NetworkIngress:  100,
  1003  					NetworkEgress:   100,
  1004  					Storage:         500,
  1005  					Status:          schedv1.ClusterStatus_UP,
  1006  					ServiceProvider: "aws",
  1007  					Region:          "us-west-2",
  1008  					Endpoint:        "SASL_SSL://kafka-endpoint",
  1009  					ApiEndpoint:     "http://kafka-api-url",
  1010  				},
  1011  			})
  1012  			require.NoError(t, err)
  1013  		}
  1014  		// Update client call
  1015  		if r.Method == "PUT" {
  1016  			req := &schedv1.UpdateKafkaClusterRequest{}
  1017  			err := utilv1.UnmarshalJSON(r.Body, req)
  1018  			require.NoError(t, err)
  1019  			if req.Cluster.Cku > 0 {
  1020  				out, err = utilv1.MarshalJSONToBytes(&schedv1.GetKafkaClusterReply{
  1021  					Cluster: nil,
  1022  					Error: &corev1.Error{
  1023  						Message: "cluster expansion is supported for dedicated clusters only",
  1024  					},
  1025  				})
  1026  			} else {
  1027  				out, err = utilv1.MarshalJSONToBytes(&schedv1.GetKafkaClusterReply{
  1028  					Cluster: &schedv1.KafkaCluster{
  1029  						Id:              req.Cluster.Id,
  1030  						Name:            req.Cluster.Name,
  1031  						Deployment:      &schedv1.Deployment{Sku: productv1.Sku_BASIC},
  1032  						NetworkIngress:  100,
  1033  						NetworkEgress:   100,
  1034  						Storage:         500,
  1035  						Status:          schedv1.ClusterStatus_UP,
  1036  						ServiceProvider: "aws",
  1037  						Region:          "us-west-2",
  1038  						Endpoint:        "SASL_SSL://kafka-endpoint",
  1039  						ApiEndpoint:     "http://kafka-api-url",
  1040  					},
  1041  				})
  1042  			}
  1043  			require.NoError(t, err)
  1044  		}
  1045  		_, err := io.WriteString(w, string(out))
  1046  		require.NoError(t, err)
  1047  	}
  1048  }
  1049  
  1050  func handleKafkaDedicatedClusterUpdateTest(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
  1051  	return func(w http.ResponseWriter, r *http.Request) {
  1052  		var out []byte
  1053  		if r.Method == "GET" {
  1054  			id := r.URL.Query().Get("id")
  1055  			var err error
  1056  			out, err = utilv1.MarshalJSONToBytes(&schedv1.GetKafkaClusterReply{
  1057  				Cluster: &schedv1.KafkaCluster{
  1058  					Id:              id,
  1059  					Name:            "lkc-update-dedicated",
  1060  					Cku:             1,
  1061  					Deployment:      &schedv1.Deployment{Sku: productv1.Sku_DEDICATED},
  1062  					NetworkIngress:  50,
  1063  					NetworkEgress:   150,
  1064  					Storage:         30000,
  1065  					Status:          schedv1.ClusterStatus_EXPANDING,
  1066  					ServiceProvider: "aws",
  1067  					Region:          "us-west-2",
  1068  					Endpoint:        "SASL_SSL://kafka-endpoint",
  1069  					ApiEndpoint:     "http://kafka-api-url",
  1070  				},
  1071  			})
  1072  			require.NoError(t, err)
  1073  		}
  1074  		// Update client call
  1075  		if r.Method == "PUT" {
  1076  			req := &schedv1.UpdateKafkaClusterRequest{}
  1077  			err := utilv1.UnmarshalJSON(r.Body, req)
  1078  			require.NoError(t, err)
  1079  			out, err = utilv1.MarshalJSONToBytes(&schedv1.GetKafkaClusterReply{
  1080  				Cluster: &schedv1.KafkaCluster{
  1081  					Id:              req.Cluster.Id,
  1082  					Name:            req.Cluster.Name,
  1083  					Cku:             1,
  1084  					PendingCku:      req.Cluster.Cku,
  1085  					Deployment:      &schedv1.Deployment{Sku: productv1.Sku_DEDICATED},
  1086  					NetworkIngress:  50 * req.Cluster.Cku,
  1087  					NetworkEgress:   150 * req.Cluster.Cku,
  1088  					Storage:         30000 * req.Cluster.Cku,
  1089  					Status:          schedv1.ClusterStatus_EXPANDING,
  1090  					ServiceProvider: "aws",
  1091  					Region:          "us-west-2",
  1092  					Endpoint:        "SASL_SSL://kafka-endpoint",
  1093  					ApiEndpoint:     "http://kafka-api-url",
  1094  				},
  1095  			})
  1096  			require.NoError(t, err)
  1097  		}
  1098  		_, err := io.WriteString(w, string(out))
  1099  		require.NoError(t, err)
  1100  	}
  1101  }
  1102  
  1103  func handleKafkaClusterCreate(t *testing.T, kafkaAPIURL string) func(w http.ResponseWriter, r *http.Request) {
  1104  	return func(w http.ResponseWriter, r *http.Request) {
  1105  		req := &schedv1.CreateKafkaClusterRequest{}
  1106  		err := utilv1.UnmarshalJSON(r.Body, req)
  1107  		require.NoError(t, err)
  1108  		var b []byte
  1109  		if req.Config.Deployment.Sku == productv1.Sku_DEDICATED {
  1110  			b, err = utilv1.MarshalJSONToBytes(&schedv1.GetKafkaClusterReply{
  1111  				Cluster: &schedv1.KafkaCluster{
  1112  					Id:              "lkc-def963",
  1113  					AccountId:       req.Config.AccountId,
  1114  					Name:            req.Config.Name,
  1115  					Cku:             req.Config.Cku,
  1116  					Deployment:      &schedv1.Deployment{Sku: productv1.Sku_DEDICATED},
  1117  					NetworkIngress:  50 * req.Config.Cku,
  1118  					NetworkEgress:   150 * req.Config.Cku,
  1119  					Storage:         30000 * req.Config.Cku,
  1120  					ServiceProvider: req.Config.ServiceProvider,
  1121  					Region:          req.Config.Region,
  1122  					Endpoint:        "SASL_SSL://kafka-endpoint",
  1123  					ApiEndpoint:     kafkaAPIURL,
  1124  				},
  1125  			})
  1126  		} else {
  1127  			b, err = utilv1.MarshalJSONToBytes(&schedv1.GetKafkaClusterReply{
  1128  				Cluster: &schedv1.KafkaCluster{
  1129  					Id:              "lkc-def963",
  1130  					AccountId:       req.Config.AccountId,
  1131  					Name:            req.Config.Name,
  1132  					Deployment:      &schedv1.Deployment{Sku: productv1.Sku_BASIC},
  1133  					NetworkIngress:  100,
  1134  					NetworkEgress:   100,
  1135  					Storage:         5000,
  1136  					ServiceProvider: req.Config.ServiceProvider,
  1137  					Region:          req.Config.Region,
  1138  					Endpoint:        "SASL_SSL://kafka-endpoint",
  1139  					ApiEndpoint:     kafkaAPIURL,
  1140  				},
  1141  			})
  1142  		}
  1143  		require.NoError(t, err)
  1144  		_, err = io.WriteString(w, string(b))
  1145  		require.NoError(t, err)
  1146  	}
  1147  }
  1148  
  1149  func handleKafkaACLsList(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
  1150  	return func(w http.ResponseWriter, r *http.Request) {
  1151  		results := []*schedv1.ACLBinding{
  1152  			{
  1153  				Pattern: &schedv1.ResourcePatternConfig{
  1154  					ResourceType: schedv1.ResourceTypes_TOPIC,
  1155  					Name:         "test-topic",
  1156  					PatternType:  schedv1.PatternTypes_LITERAL,
  1157  				},
  1158  				Entry: &schedv1.AccessControlEntryConfig{
  1159  					Operation:      schedv1.ACLOperations_READ,
  1160  					PermissionType: schedv1.ACLPermissionTypes_ALLOW,
  1161  				},
  1162  			},
  1163  		}
  1164  		reply, err := json.Marshal(results)
  1165  		require.NoError(t, err)
  1166  		_, err = io.WriteString(w, string(reply))
  1167  		require.NoError(t, err)
  1168  	}
  1169  }
  1170  
  1171  func handleKafkaACLsCreate(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
  1172  	return func(w http.ResponseWriter, r *http.Request) {
  1173  		if r.Method == "POST" {
  1174  			var bindings []*schedv1.ACLBinding
  1175  			err := json.NewDecoder(r.Body).Decode(&bindings)
  1176  			require.NoError(t, err)
  1177  			require.NotEmpty(t, bindings)
  1178  			for _, binding := range bindings {
  1179  				require.NotEmpty(t, binding.GetPattern())
  1180  				require.NotEmpty(t, binding.GetEntry())
  1181  			}
  1182  		}
  1183  	}
  1184  }
  1185  
  1186  func handleKafkaACLsDelete(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
  1187  	return func(w http.ResponseWriter, r *http.Request) {
  1188  		var filters []*schedv1.ACLFilter
  1189  		err := json.NewDecoder(r.Body).Decode(&filters)
  1190  		require.NoError(t, err)
  1191  		require.NotEmpty(t, filters)
  1192  		for _, filter := range filters {
  1193  			require.NotEmpty(t, filter.GetEntryFilter())
  1194  			require.NotEmpty(t, filter.GetPatternFilter())
  1195  		}
  1196  	}
  1197  }
  1198  
  1199  func handleKSQLCreateList(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
  1200  	return func(w http.ResponseWriter, r *http.Request) {
  1201  		ksqlCluster1 := &schedv1.KSQLCluster{
  1202  			Id:                "lksqlc-ksql5",
  1203  			AccountId:         "25",
  1204  			KafkaClusterId:    "lkc-qwert",
  1205  			OutputTopicPrefix: "pksqlc-abcde",
  1206  			Name:              "account ksql",
  1207  			Storage:           101,
  1208  			Endpoint:          "SASL_SSL://ksql-endpoint",
  1209  		}
  1210  		ksqlCluster2 := &schedv1.KSQLCluster{
  1211  			Id:                "lksqlc-woooo",
  1212  			AccountId:         "25",
  1213  			KafkaClusterId:    "lkc-zxcvb",
  1214  			OutputTopicPrefix: "pksqlc-ghjkl",
  1215  			Name:              "kay cee queue elle",
  1216  			Storage:           123,
  1217  			Endpoint:          "SASL_SSL://ksql-endpoint",
  1218  		}
  1219  		if r.Method == "POST" {
  1220  			reply, err := utilv1.MarshalJSONToBytes(&schedv1.GetKSQLClusterReply{
  1221  				Cluster: ksqlCluster1,
  1222  			})
  1223  			require.NoError(t, err)
  1224  			_, err = io.WriteString(w, string(reply))
  1225  			require.NoError(t, err)
  1226  		} else if r.Method == "GET" {
  1227  			listReply, err := utilv1.MarshalJSONToBytes(&schedv1.GetKSQLClustersReply{
  1228  				Clusters: []*schedv1.KSQLCluster{ksqlCluster1, ksqlCluster2},
  1229  			})
  1230  			require.NoError(t, err)
  1231  			_, err = io.WriteString(w, string(listReply))
  1232  			require.NoError(t, err)
  1233  		}
  1234  	}
  1235  }
  1236  
  1237  func handleConnect(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
  1238  	return func(w http.ResponseWriter, r *http.Request) {
  1239  		if r.Method == "GET" {
  1240  			connectorExpansion := &opv1.ConnectorExpansion{
  1241  				Id: &opv1.ConnectorId{Id: "lcc-123"},
  1242  				Info: &opv1.ConnectorInfo{
  1243  					Name:   "az-connector",
  1244  					Type:   "Sink",
  1245  					Config: map[string]string{},
  1246  				},
  1247  				Status: &opv1.ConnectorStateInfo{Name: "az-connector", Connector: &opv1.ConnectorState{State: "Running"},
  1248  					Tasks: []*opv1.TaskState{{Id: 1, State: "Running"}},
  1249  				}}
  1250  			listReply, err := json.Marshal(map[string]*opv1.ConnectorExpansion{"lcc-123": connectorExpansion})
  1251  			require.NoError(t, err)
  1252  			_, err = io.WriteString(w, string(listReply))
  1253  			require.NoError(t, err)
  1254  		} else if r.Method == "POST" {
  1255  			var request opv1.ConnectorInfo
  1256  			err := utilv1.UnmarshalJSON(r.Body, &request)
  1257  			require.NoError(t, err)
  1258  			connector1 := &schedv1.Connector{
  1259  				Name:           request.Name,
  1260  				KafkaClusterId: "lkc-123",
  1261  				AccountId:      "a-595",
  1262  				UserConfigs:    request.Config,
  1263  				Plugin:         request.Config["connector.class"],
  1264  			}
  1265  			reply, err := utilv1.MarshalJSONToBytes(connector1)
  1266  			require.NoError(t, err)
  1267  			_, err = io.WriteString(w, string(reply))
  1268  			require.NoError(t, err)
  1269  		}
  1270  	}
  1271  }
  1272  
  1273  func handleConnectorCatalogDescribe(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
  1274  	return func(w http.ResponseWriter, r *http.Request) {
  1275  		configInfos := &opv1.ConfigInfos{
  1276  			Name:       "",
  1277  			Groups:     nil,
  1278  			ErrorCount: 1,
  1279  			Configs: []*opv1.Configs{
  1280  				{
  1281  					Value: &opv1.ConfigValue{
  1282  						Name:   "kafka.api.key",
  1283  						Errors: []string{"\"kafka.api.key\" is required"},
  1284  					},
  1285  				},
  1286  				{
  1287  					Value: &opv1.ConfigValue{
  1288  						Name:   "kafka.api.secret",
  1289  						Errors: []string{"\"kafka.api.secret\" is required"},
  1290  					},
  1291  				},
  1292  				{
  1293  					Value: &opv1.ConfigValue{
  1294  						Name:   "topics",
  1295  						Errors: []string{"\"topics\" is required"},
  1296  					},
  1297  				},
  1298  				{
  1299  					Value: &opv1.ConfigValue{
  1300  						Name:   "data.format",
  1301  						Errors: []string{"\"data.format\" is required", "Value \"null\" doesn't belong to the property's \"data.format\" enum"},
  1302  					},
  1303  				},
  1304  				{
  1305  					Value: &opv1.ConfigValue{
  1306  						Name:   "gcs.credentials.config",
  1307  						Errors: []string{"\"gcs.credentials.config\" is required"},
  1308  					},
  1309  				},
  1310  				{
  1311  					Value: &opv1.ConfigValue{
  1312  						Name:   "gcs.bucket.name",
  1313  						Errors: []string{"\"gcs.bucket.name\" is required"},
  1314  					},
  1315  				},
  1316  				{
  1317  					Value: &opv1.ConfigValue{
  1318  						Name:   "time.interval",
  1319  						Errors: []string{"\"data.format\" is required", "Value \"null\" doesn't belong to the property's \"time.interval\" enum"},
  1320  					},
  1321  				},
  1322  				{
  1323  					Value: &opv1.ConfigValue{
  1324  						Name:   "tasks.max",
  1325  						Errors: []string{"\"tasks.max\" is required"},
  1326  					},
  1327  				},
  1328  			},
  1329  		}
  1330  		reply, err := json.Marshal(configInfos)
  1331  		require.NoError(t, err)
  1332  		_, err = io.WriteString(w, string(reply))
  1333  		require.NoError(t, err)
  1334  	}
  1335  }
  1336  
  1337  func handleConnectPlugins(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
  1338  	return func(w http.ResponseWriter, r *http.Request) {
  1339  		if r.Method == "GET" {
  1340  			connectorPlugin1 := &opv1.ConnectorPluginInfo{
  1341  				Class: "AzureBlobSink",
  1342  				Type:  "Sink",
  1343  			}
  1344  			connectorPlugin2 := &opv1.ConnectorPluginInfo{
  1345  				Class: "GcsSink",
  1346  				Type:  "Sink",
  1347  			}
  1348  			listReply, err := json.Marshal([]*opv1.ConnectorPluginInfo{connectorPlugin1, connectorPlugin2})
  1349  			require.NoError(t, err)
  1350  			_, err = io.WriteString(w, string(listReply))
  1351  			require.NoError(t, err)
  1352  		}
  1353  	}
  1354  }
  1355  
  1356  func compose(funcs ...func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
  1357  	return func(w http.ResponseWriter, r *http.Request) {
  1358  		for _, f := range funcs {
  1359  			f(w, r)
  1360  		}
  1361  	}
  1362  }
  1363  
  1364  func handleEnvironmentRequests(t *testing.T, id string) func(w http.ResponseWriter, r *http.Request) {
  1365  	return func(w http.ResponseWriter, r *http.Request) {
  1366  		for _, env := range environments {
  1367  			if env.Id == id {
  1368  				// env found
  1369  				if r.Method == "GET" {
  1370  					b, err := utilv1.MarshalJSONToBytes(&orgv1.GetAccountReply{Account: env})
  1371  					require.NoError(t, err)
  1372  					_, err = io.WriteString(w, string(b))
  1373  					require.NoError(t, err)
  1374  				} else if r.Method == "PUT" {
  1375  					req := &orgv1.UpdateAccountRequest{}
  1376  					err := utilv1.UnmarshalJSON(r.Body, req)
  1377  					require.NoError(t, err)
  1378  					env.Name = req.Account.Name
  1379  					b, err := utilv1.MarshalJSONToBytes(&orgv1.UpdateAccountReply{Account: env})
  1380  					require.NoError(t, err)
  1381  					_, err = io.WriteString(w, string(b))
  1382  					require.NoError(t, err)
  1383  				} else if r.Method == "DELETE" {
  1384  					b, err := utilv1.MarshalJSONToBytes(&orgv1.DeleteAccountReply{})
  1385  					require.NoError(t, err)
  1386  					_, err = io.WriteString(w, string(b))
  1387  					require.NoError(t, err)
  1388  				}
  1389  				return
  1390  			}
  1391  		}
  1392  		// env not found
  1393  		w.WriteHeader(http.StatusNotFound)
  1394  	}
  1395  }
  1396  
  1397  func handleAPIKeyUpdateAndDelete(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
  1398  	return func(w http.ResponseWriter, r *http.Request) {
  1399  		urlSplit := strings.Split(r.URL.Path, "/")
  1400  		keyId, err := strconv.Atoi(urlSplit[len(urlSplit)-1])
  1401  		require.NoError(t, err)
  1402  		index := int32(keyId)
  1403  		apiKey := keyStore[index]
  1404  		if r.Method == "PUT" {
  1405  			req := &schedv1.UpdateApiKeyRequest{}
  1406  			err = utilv1.UnmarshalJSON(r.Body, req)
  1407  			require.NoError(t, err)
  1408  			apiKey.Description = req.ApiKey.Description
  1409  			result := &schedv1.UpdateApiKeyReply{
  1410  				ApiKey: apiKey,
  1411  				Error:  nil,
  1412  			}
  1413  			reply, err := json.Marshal(result)
  1414  			require.NoError(t, err)
  1415  			_, err = io.WriteString(w, string(reply))
  1416  			require.NoError(t, err)
  1417  		} else if r.Method == "DELETE" {
  1418  			req := &schedv1.DeleteApiKeyRequest{}
  1419  			err = utilv1.UnmarshalJSON(r.Body, req)
  1420  			require.NoError(t, err)
  1421  			delete(keyStore, index)
  1422  			result := &schedv1.DeleteApiKeyReply{
  1423  				ApiKey: apiKey,
  1424  				Error:  nil,
  1425  			}
  1426  			reply, err := json.Marshal(result)
  1427  			require.NoError(t, err)
  1428  			_, err = io.WriteString(w, string(reply))
  1429  			require.NoError(t, err)
  1430  		}
  1431  
  1432  	}
  1433  }
  1434  
  1435  func handleServiceAccountRequests(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
  1436  	return func(w http.ResponseWriter, r *http.Request) {
  1437  		switch r.Method {
  1438  		case "GET":
  1439  			serviceAccount := &orgv1.User{
  1440  				Id:                 12345,
  1441  				ServiceName:        "service_account",
  1442  				ServiceDescription: "at your service.",
  1443  			}
  1444  			listReply, err := utilv1.MarshalJSONToBytes(&orgv1.GetServiceAccountsReply{
  1445  				Users: []*orgv1.User{serviceAccount},
  1446  			})
  1447  			require.NoError(t, err)
  1448  			_, err = io.WriteString(w, string(listReply))
  1449  			require.NoError(t, err)
  1450  		case "POST":
  1451  			req := &orgv1.CreateServiceAccountRequest{}
  1452  			err := utilv1.UnmarshalJSON(r.Body, req)
  1453  			require.NoError(t, err)
  1454  			serviceAccount := &orgv1.User{
  1455  				Id:                 55555,
  1456  				ServiceName:        req.User.ServiceName,
  1457  				ServiceDescription: req.User.ServiceDescription,
  1458  			}
  1459  			createReply, err := utilv1.MarshalJSONToBytes(&orgv1.CreateServiceAccountReply{
  1460  				Error: nil,
  1461  				User:  serviceAccount,
  1462  			})
  1463  			require.NoError(t, err)
  1464  			_, err = io.WriteString(w, string(createReply))
  1465  			require.NoError(t, err)
  1466  		case "PUT":
  1467  			req := &orgv1.UpdateServiceAccountRequest{}
  1468  			err := utilv1.UnmarshalJSON(r.Body, req)
  1469  			require.NoError(t, err)
  1470  			updateReply, err := utilv1.MarshalJSONToBytes(&orgv1.UpdateServiceAccountReply{
  1471  				Error: nil,
  1472  				User:  req.User,
  1473  			})
  1474  			require.NoError(t, err)
  1475  			_, err = io.WriteString(w, string(updateReply))
  1476  			require.NoError(t, err)
  1477  		case "DELETE":
  1478  			req := &orgv1.DeleteServiceAccountRequest{}
  1479  			err := utilv1.UnmarshalJSON(r.Body, req)
  1480  			require.NoError(t, err)
  1481  			updateReply, err := utilv1.MarshalJSONToBytes(&orgv1.DeleteServiceAccountReply{
  1482  				Error: nil,
  1483  			})
  1484  			require.NoError(t, err)
  1485  			_, err = io.WriteString(w, string(updateReply))
  1486  			require.NoError(t, err)
  1487  		}
  1488  	}
  1489  }