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 }