package e2emutate import ( "context" "encoding/json" "errors" "io" "net/http" "strings" "testing" ) type fakeHTTP struct { do func(*http.Request) (*http.Response, error) calls []*http.Request } func (f *fakeHTTP) Do(request *http.Request) (*http.Response, error) { f.calls = append(f.calls, request) return f.do(request) } func mutationResponse(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{ ForgejoURL: "https://git.example.test", Owner: "Maidn", Repo: "maidn-e2e-repo", Branch: "maidn-e2e-branch", SHA: "0123456789abcdef0123456789abcdef01234567", Token: "test-token", } } func TestOptionsValidateAcceptsOnlyFixtureTargets(t *testing.T) { if err := testOptions().Validate(); err != nil { t.Fatalf("valid fixture options: %v", err) } for _, update := range []func(*Options){ func(o *Options) { o.Owner = "other-org" }, func(o *Options) { o.Repo = "production" }, func(o *Options) { o.Branch = "feature/maidn-e2e-branch" }, func(o *Options) { o.Branch = "maidn-e2e-branch..unsafe" }, func(o *Options) { o.Repo = "maidn-e2e-repo.lock" }, } { options := testOptions() update(&options) if err := options.Validate(); err == nil { t.Fatalf("Validate accepted %#v", options) } } } func TestMutatorUpdatesFixtureRefAndEnsuresOnePR(t *testing.T) { fake := &fakeHTTP{do: func(request *http.Request) (*http.Response, error) { if request.Header.Get("Authorization") != "token test-token" { t.Fatal("mutation request did not authenticate at the API boundary") } switch { case request.Method == http.MethodPatch && request.URL.Path == "/api/v1/repos/Maidn/maidn-e2e-repo/git/refs/heads/maidn-e2e-branch": var body struct { SHA string `json:"sha"` Force bool `json:"force"` } if err := json.NewDecoder(request.Body).Decode(&body); err != nil || body.SHA != testOptions().SHA || body.Force { t.Fatalf("unexpected branch update: %#v, %v", body, err) } return mutationResponse(http.StatusOK, ""), nil case request.Method == http.MethodGet && request.URL.Path == "/api/v1/repos/Maidn/maidn-e2e-repo/pulls": if request.URL.Query().Get("head") != "Maidn:maidn-e2e-branch" || request.URL.Query().Get("state") != "open" { t.Fatal("pull request lookup did not target the fixture branch") } return mutationResponse(http.StatusOK, "[]"), nil case request.Method == http.MethodPost && request.URL.Path == "/api/v1/repos/Maidn/maidn-e2e-repo/pulls": var body struct { Head string `json:"head"` Base string `json:"base"` } if err := json.NewDecoder(request.Body).Decode(&body); err != nil || body.Head != "maidn-e2e-branch" || body.Base != "main" { t.Fatalf("unexpected pull request creation: %#v, %v", body, err) } return mutationResponse(http.StatusCreated, ""), nil default: t.Fatalf("unexpected Forgejo request: %s %s", request.Method, request.URL) return nil, nil } }} options := testOptions() options.OpenPR = true if err := (Mutator{HTTP: fake}).Run(context.Background(), options); err != nil || len(fake.calls) != 3 { t.Fatalf("Run() = %v, calls = %d", err, len(fake.calls)) } } func TestMutatorCreatesFixtureRefWhenAbsent(t *testing.T) { fake := &fakeHTTP{do: func(request *http.Request) (*http.Response, error) { switch request.Method { case http.MethodPatch: return mutationResponse(http.StatusNotFound, ""), nil case http.MethodPost: if request.URL.Path != "/api/v1/repos/Maidn/maidn-e2e-repo/git/refs" { t.Fatalf("branch creation targeted %q", request.URL.Path) } return mutationResponse(http.StatusCreated, ""), nil default: t.Fatalf("unexpected Forgejo request: %s %s", request.Method, request.URL) return nil, nil } }} if err := (Mutator{HTTP: fake}).Run(context.Background(), testOptions()); err != nil || len(fake.calls) != 2 { t.Fatalf("Run() = %v, calls = %d", err, len(fake.calls)) } } func TestMutatorRejectsUnsafeTargetsBeforeAPIAndDoesNotExposeToken(t *testing.T) { fake := &fakeHTTP{do: func(*http.Request) (*http.Response, error) { t.Fatal("unsafe target reached the Forgejo API") return nil, nil }} options := testOptions() options.Owner = "production" options.Token = "secret-token" err := (Mutator{HTTP: fake}).Run(context.Background(), options) if err == nil || strings.Contains(err.Error(), options.Token) { t.Fatalf("Run() returned unsafe error: %v", err) } fake.do = func(*http.Request) (*http.Response, error) { return nil, errors.New(options.Token) } options = testOptions() options.Token = "secret-token" err = (Mutator{HTTP: fake}).Run(context.Background(), options) if err == nil || strings.Contains(err.Error(), options.Token) { t.Fatalf("Run() exposed token: %v", err) } }