package e2e import ( "context" "encoding/json" "io" "net/http" "strings" "testing" "time" ) type fakeCommand struct { output func(string, []string) ([]byte, error) calls [][]string } func (f *fakeCommand) Output(_ context.Context, name string, args ...string) ([]byte, error) { f.calls = append(f.calls, append([]string{name}, args...)) return f.output(name, args) } type fakeHTTP struct { do func(*http.Request) (*http.Response, error) } func (f fakeHTTP) Do(request *http.Request) (*http.Response, error) { return f.do(request) } func response(status int, body string) *http.Response { return &http.Response{StatusCode: status, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)} } func testOptions() Options { return Options{ Kubeconfig: "/run/secrets/kubeconfig", FluxKustomizations: []string{"flux-system/tekton"}, ExternalSecret: "tekton-pipelines/forgejo-webhook", PipelineRun: "tekton-pipelines/delivery-1", PreviewURL: "https://preview.example.test/", PreviewSentinel: "maidn-e2e-ok", PromotionPullsURL: "https://git.example.test/api/v1/repos/Maidn/manifests/pulls", PromotionOwner: "Maidn", PromotionHead: "maidn/promotion-app-0123456789abcdef0123456789abcdef01234567", PromotionToken: "test-token", Timeout: time.Second, Interval: time.Millisecond, } } func TestRunUsesReadOnlyBoundariesAndRedactsResponses(t *testing.T) { kubectl := &fakeCommand{output: func(_ string, args []string) ([]byte, error) { if strings.Contains(strings.Join(args, " "), "pipelineruns.tekton.dev") { return []byte(`{"status":{"conditions":[{"type":"Succeeded","status":"True"}]}}`), nil } return []byte(`{"status":{"conditions":[{"type":"Ready","status":"True"}]},"data":"secret-value"}`), nil }} http := fakeHTTP{do: func(request *http.Request) (*http.Response, error) { if strings.Contains(request.URL.Path, "/pulls") { if request.Header.Get("Authorization") != "token test-token" { t.Fatal("promotion request did not use the supplied token") } if got := request.URL.Query().Get("head"); got != "Maidn:maidn/promotion-app-0123456789abcdef0123456789abcdef01234567" { t.Fatalf("promotion head = %q", got) } return response(http.StatusOK, `[{"state":"open","body":"secret-value"}]`), nil } return response(http.StatusOK, "maidn-e2e-ok secret-value"), nil }} result, err := (Runner{Kubectl: kubectl, HTTP: http}).Run(context.Background(), testOptions()) if err != nil || !result.Passed || len(result.Checks) != 5 { t.Fatalf("Run() = %#v, %v", result, err) } encoded, err := json.Marshal(result) if err != nil { t.Fatal(err) } if strings.Contains(string(encoded), "secret-value") || strings.Contains(string(encoded), "test-token") { t.Fatalf("result exposed response data: %s", encoded) } for _, call := range kubectl.calls { joined := strings.Join(call, " ") if !strings.Contains(joined, " get ") || strings.Contains(joined, "apply") || strings.Contains(joined, "reconcile") { t.Fatalf("unexpected kubectl invocation: %q", joined) } } } func TestRunReportsTerminalPipelineFailureWithoutWaiting(t *testing.T) { kubectl := &fakeCommand{output: func(_ string, args []string) ([]byte, error) { if strings.Contains(strings.Join(args, " "), "pipelineruns.tekton.dev") { return []byte(`{"status":{"conditions":[{"type":"Succeeded","status":"False"}]}}`), nil } return []byte(`{"status":{"conditions":[{"type":"Ready","status":"True"}]}}`), nil }} http := fakeHTTP{do: func(request *http.Request) (*http.Response, error) { if strings.Contains(request.URL.Path, "/pulls") { return response(http.StatusOK, `[{"state":"open"}]`), nil } return response(http.StatusOK, "maidn-e2e-ok"), nil }} result, err := (Runner{Kubectl: kubectl, HTTP: http}).Run(context.Background(), testOptions()) if err != nil || result.Passed || result.Checks[2].Detail != "failed" { t.Fatalf("Run() = %#v, %v", result, err) } } func TestRunTimesOutWhenAReadinessConditionNeverArrives(t *testing.T) { kubectl := &fakeCommand{output: func(_ string, args []string) ([]byte, error) { if strings.Contains(strings.Join(args, " "), "externalsecrets.external-secrets.io") { return []byte(`{"status":{"conditions":[{"type":"Ready","status":"False"}]}}`), nil } if strings.Contains(strings.Join(args, " "), "pipelineruns.tekton.dev") { return []byte(`{"status":{"conditions":[{"type":"Succeeded","status":"True"}]}}`), nil } return []byte(`{"status":{"conditions":[{"type":"Ready","status":"True"}]}}`), nil }} http := fakeHTTP{do: func(request *http.Request) (*http.Response, error) { if strings.Contains(request.URL.Path, "/pulls") { return response(http.StatusOK, `[{"state":"open"}]`), nil } return response(http.StatusOK, "maidn-e2e-ok"), nil }} options := testOptions() options.Timeout, options.Interval = 5*time.Millisecond, time.Millisecond result, err := (Runner{Kubectl: kubectl, HTTP: http}).Run(context.Background(), options) if err != nil || result.Passed || result.Checks[1].Detail != "timed_out" { t.Fatalf("Run() = %#v, %v", result, err) } } func TestReadTokenRejectsAmbiguousReferences(t *testing.T) { if _, err := ReadToken("PROMOTION_TOKEN", "token.txt"); err == nil { t.Fatal("ReadToken accepted two token references") } }