|
| 1 | +package cmd |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/json" |
| 6 | + "io" |
| 7 | + "net/http" |
| 8 | + "net/http/httptest" |
| 9 | + "testing" |
| 10 | + "time" |
| 11 | + |
| 12 | + "github.com/kernel/cli/pkg/auth" |
| 13 | + kernel "github.com/kernel/kernel-go-sdk" |
| 14 | + "github.com/spf13/cobra" |
| 15 | + "github.com/stretchr/testify/assert" |
| 16 | + "github.com/stretchr/testify/require" |
| 17 | + "github.com/zalando/go-keyring" |
| 18 | +) |
| 19 | + |
| 20 | +func projectTestCommand(t *testing.T, serverURL, project string) *cobra.Command { |
| 21 | + t.Helper() |
| 22 | + t.Setenv("KERNEL_BASE_URL", serverURL) |
| 23 | + t.Setenv("KERNEL_API_KEY", "test-api-key") |
| 24 | + cmd := &cobra.Command{Use: "request"} |
| 25 | + cmd.SetContext(context.Background()) |
| 26 | + cmd.Flags().String("project", project, "") |
| 27 | + cmd.Flags().String("log-level", "warn", "") |
| 28 | + cmd.Flags().Bool("no-color", false, "") |
| 29 | + return cmd |
| 30 | +} |
| 31 | + |
| 32 | +func TestProjectSelectionHeaders(t *testing.T) { |
| 33 | + for _, tt := range []struct { |
| 34 | + name, flag, env, want string |
| 35 | + }{ |
| 36 | + {name: "flag ID", flag: "proj_123", want: "proj_123"}, |
| 37 | + {name: "flag exact name", flag: "Release QA", want: "Release QA"}, |
| 38 | + {name: "environment ID", env: "proj_123", want: "proj_123"}, |
| 39 | + {name: "environment exact name", env: "Release QA", want: "Release QA"}, |
| 40 | + {name: "flag wins", flag: "Release QA", env: "proj_other", want: "Release QA"}, |
| 41 | + {name: "legacy name delimiters", flag: "QA/100%", want: "QA/100%"}, |
| 42 | + {name: "no selection"}, |
| 43 | + } { |
| 44 | + t.Run(tt.name, func(t *testing.T) { |
| 45 | + t.Setenv("KERNEL_PROJECT", tt.env) |
| 46 | + var paths []string |
| 47 | + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 48 | + paths = append(paths, r.URL.Path) |
| 49 | + assert.Equal(t, tt.want, r.Header.Get("X-Kernel-Project")) |
| 50 | + assert.Empty(t, r.Header.Get("X-Kernel-Project-Id")) |
| 51 | + assert.Equal(t, "Bearer test-api-key", r.Header.Get("Authorization")) |
| 52 | + w.Header().Set("Content-Type", "application/json") |
| 53 | + _, _ = io.WriteString(w, `{}`) |
| 54 | + })) |
| 55 | + defer server.Close() |
| 56 | + cmd := projectTestCommand(t, server.URL, tt.flag) |
| 57 | + require.NoError(t, rootCmd.PersistentPreRunE(cmd, nil)) |
| 58 | + client := getKernelClient(cmd) |
| 59 | + _, err := client.Browsers.Get(cmd.Context(), "browser_123", kernel.BrowserGetParams{}) |
| 60 | + require.NoError(t, err) |
| 61 | + _, err = client.Profiles.Get(cmd.Context(), "profile_123") |
| 62 | + require.NoError(t, err) |
| 63 | + _, err = client.Proxies.Get(cmd.Context(), "proxy_123") |
| 64 | + require.NoError(t, err) |
| 65 | + _, err = client.Auth.Connections.Get(cmd.Context(), "connection_123") |
| 66 | + require.NoError(t, err) |
| 67 | + assert.Equal(t, []string{"/browsers/browser_123", "/profiles/profile_123", "/proxies/proxy_123", "/auth/connections/connection_123"}, paths) |
| 68 | + }) |
| 69 | + } |
| 70 | +} |
| 71 | + |
| 72 | +func TestDeployGithubProjectScope(t *testing.T) { |
| 73 | + for _, tt := range []struct { |
| 74 | + name, flag, env, want string |
| 75 | + oauth bool |
| 76 | + status int |
| 77 | + }{ |
| 78 | + {name: "flag ID", flag: "proj_123", want: "proj_123"}, |
| 79 | + {name: "flag exact name", flag: "Release QA", want: "Release QA"}, |
| 80 | + {name: "environment ID", env: "proj_123", want: "proj_123"}, |
| 81 | + {name: "environment exact name", env: "Release QA", want: "Release QA"}, |
| 82 | + {name: "flag wins", flag: "Release QA", env: "proj_other", want: "Release QA"}, |
| 83 | + {name: "no selection"}, |
| 84 | + {name: "project OAuth", flag: "Release QA", want: "Release QA", oauth: true}, |
| 85 | + {name: "server error without retry", flag: "proj_123", want: "proj_123", status: http.StatusInternalServerError}, |
| 86 | + {name: "invalid ID", flag: "proj_missing", want: "proj_missing", status: http.StatusNotFound}, |
| 87 | + {name: "invalid name", env: "missing project", want: "missing project", status: http.StatusNotFound}, |
| 88 | + {name: "credential scope mismatch", flag: "Other Project", want: "Other Project", oauth: true, status: http.StatusForbidden}, |
| 89 | + } { |
| 90 | + t.Run(tt.name, func(t *testing.T) { |
| 91 | + t.Setenv("KERNEL_PROJECT", tt.env) |
| 92 | + wantAuth := "Bearer test-api-key" |
| 93 | + if tt.oauth { |
| 94 | + wantAuth = "Bearer test-oauth-token" |
| 95 | + } |
| 96 | + var paths []string |
| 97 | + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 98 | + paths = append(paths, r.Method+" "+r.URL.Path) |
| 99 | + assert.Equal(t, tt.want, r.Header.Get("X-Kernel-Project")) |
| 100 | + assert.Empty(t, r.Header.Get("X-Kernel-Project-Id")) |
| 101 | + assert.Equal(t, wantAuth, r.Header.Get("Authorization")) |
| 102 | + assert.Equal(t, metadata.Version, r.Header.Get("X-Kernel-Cli-Version")) |
| 103 | + switch r.URL.Path { |
| 104 | + case "/deployments": |
| 105 | + assert.Equal(t, http.MethodPost, r.Method) |
| 106 | + if err := r.ParseMultipartForm(1 << 20); !assert.NoError(t, err) { |
| 107 | + w.WriteHeader(http.StatusBadRequest) |
| 108 | + return |
| 109 | + } |
| 110 | + defer func() { assert.NoError(t, r.MultipartForm.RemoveAll()) }() |
| 111 | + assert.Equal(t, "aws.us-east-1a", r.FormValue("region")) |
| 112 | + var source map[string]string |
| 113 | + assert.NoError(t, json.Unmarshal([]byte(r.FormValue("source")), &source)) |
| 114 | + assert.Equal(t, map[string]string{"type": "github", "url": "https://github.com/example/app", "ref": "main", "entrypoint": "app.ts"}, source) |
| 115 | + w.Header().Set("Content-Type", "application/json") |
| 116 | + if tt.status != 0 { |
| 117 | + w.WriteHeader(tt.status) |
| 118 | + _, _ = io.WriteString(w, `{"code":"project_error","message":"project selector rejected"}`) |
| 119 | + return |
| 120 | + } |
| 121 | + _, _ = io.WriteString(w, `{"id":"deployment_123"}`) |
| 122 | + case "/deployments/deployment_123/events": |
| 123 | + w.Header().Set("Content-Type", "text/event-stream") |
| 124 | + _, _ = io.WriteString(w, "data: {\"event\":\"deployment_state\",\"deployment\":{\"status\":\"running\"}}\n\n") |
| 125 | + default: |
| 126 | + t.Errorf("unexpected request: %s", r.URL.Path) |
| 127 | + w.WriteHeader(http.StatusNotFound) |
| 128 | + } |
| 129 | + })) |
| 130 | + defer server.Close() |
| 131 | + cmd := projectTestCommand(t, server.URL, tt.flag) |
| 132 | + if tt.oauth { |
| 133 | + t.Setenv("KERNEL_API_KEY", "") |
| 134 | + keyring.MockInit() |
| 135 | + t.Cleanup(keyring.MockInit) |
| 136 | + require.NoError(t, auth.SaveTokens(&auth.TokenStorage{ |
| 137 | + AccessToken: "test-oauth-token", ExpiresAt: time.Now().Add(time.Hour), |
| 138 | + AccessScope: "project", ProjectID: "proj_123", |
| 139 | + })) |
| 140 | + } |
| 141 | + cmd.Flags().String("url", "https://github.com/example/app", "") |
| 142 | + cmd.Flags().String("ref", "main", "") |
| 143 | + cmd.Flags().String("entrypoint", "app.ts", "") |
| 144 | + cmd.Flags().String("output", "json", "") |
| 145 | + require.NoError(t, rootCmd.PersistentPreRunE(cmd, nil)) |
| 146 | + err := runDeployGithub(cmd, nil) |
| 147 | + if tt.status != 0 { |
| 148 | + var apiErr *kernel.Error |
| 149 | + require.ErrorAs(t, err, &apiErr) |
| 150 | + assert.Equal(t, tt.status, apiErr.StatusCode) |
| 151 | + assert.Contains(t, err.Error(), "project selector rejected") |
| 152 | + assert.Equal(t, []string{"POST /deployments"}, paths) |
| 153 | + } else { |
| 154 | + require.NoError(t, err) |
| 155 | + assert.Equal(t, []string{"POST /deployments", "GET /deployments/deployment_123/events"}, paths) |
| 156 | + } |
| 157 | + }) |
| 158 | + } |
| 159 | +} |
0 commit comments