feat: add scoped E2E fixture mutations #51
56
cmd/e2e_mutate.go
Normal file
56
cmd/e2e_mutate.go
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/Pingu-Studio/MaidnCLI/internal/e2emutate"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
e2eMutateForgejoURL, e2eMutateOwner, e2eMutateRepo, e2eMutateBranch, e2eMutateSHA string
|
||||||
|
e2eMutateTokenEnv, e2eMutateTokenFile string
|
||||||
|
e2eMutateOpenPR bool
|
||||||
|
e2eMutator = e2emutate.DefaultMutator
|
||||||
|
)
|
||||||
|
|
||||||
|
var e2eMutateCmd = &cobra.Command{
|
||||||
|
Use: "e2e-mutate",
|
||||||
|
Short: "Update a Forgejo E2E fixture branch and optionally open its PR.",
|
||||||
|
RunE: runE2EMutate,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(e2eMutateCmd)
|
||||||
|
e2eMutateCmd.Flags().StringVar(&e2eMutateForgejoURL, "forgejo-url", "", "Credential-free Forgejo base URL")
|
||||||
|
e2eMutateCmd.Flags().StringVar(&e2eMutateOwner, "owner", "", "Fixture Forgejo owner (must be test-org-2)")
|
||||||
|
e2eMutateCmd.Flags().StringVar(&e2eMutateRepo, "repo", "", "Fixture Forgejo repository (must start maidn-e2e-)")
|
||||||
|
e2eMutateCmd.Flags().StringVar(&e2eMutateBranch, "branch", "", "Fixture Forgejo branch (must start maidn-e2e-)")
|
||||||
|
e2eMutateCmd.Flags().StringVar(&e2eMutateSHA, "sha", "", "Full Git object ID for the fixture branch")
|
||||||
|
e2eMutateCmd.Flags().StringVar(&e2eMutateTokenEnv, "token-env", "", "Environment variable containing the Forgejo token")
|
||||||
|
e2eMutateCmd.Flags().StringVar(&e2eMutateTokenFile, "token-file", "", "Path to a file containing the Forgejo token")
|
||||||
|
e2eMutateCmd.Flags().BoolVar(&e2eMutateOpenPR, "open-pr", false, "Open one pull request from the fixture branch to main")
|
||||||
|
for _, name := range []string{"forgejo-url", "owner", "repo", "branch", "sha"} {
|
||||||
|
_ = e2eMutateCmd.MarkFlagRequired(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runE2EMutate(cmd *cobra.Command, _ []string) error {
|
||||||
|
token, err := e2emutate.ReadToken(e2eMutateTokenEnv, e2eMutateTokenFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx := cmd.Context()
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
return e2eMutator().Run(ctx, e2emutate.Options{
|
||||||
|
ForgejoURL: e2eMutateForgejoURL,
|
||||||
|
Owner: e2eMutateOwner,
|
||||||
|
Repo: e2eMutateRepo,
|
||||||
|
Branch: e2eMutateBranch,
|
||||||
|
SHA: e2eMutateSHA,
|
||||||
|
Token: token,
|
||||||
|
OpenPR: e2eMutateOpenPR,
|
||||||
|
})
|
||||||
|
}
|
||||||
46
cmd/e2e_mutate_test.go
Normal file
46
cmd/e2e_mutate_test.go
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Pingu-Studio/MaidnCLI/internal/e2emutate"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type commandMutationHTTP struct {
|
||||||
|
calls []*http.Request
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *commandMutationHTTP) Do(request *http.Request) (*http.Response, error) {
|
||||||
|
f.calls = append(f.calls, request)
|
||||||
|
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("")), Header: make(http.Header)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestE2EMutateCommandWiringUsesOnlyTokenReferences(t *testing.T) {
|
||||||
|
originalMutator := e2eMutator
|
||||||
|
originalURL, originalOwner, originalRepo, originalBranch, originalSHA := e2eMutateForgejoURL, e2eMutateOwner, e2eMutateRepo, e2eMutateBranch, e2eMutateSHA
|
||||||
|
originalEnv, originalFile, originalOpenPR := e2eMutateTokenEnv, e2eMutateTokenFile, e2eMutateOpenPR
|
||||||
|
t.Cleanup(func() {
|
||||||
|
e2eMutator = originalMutator
|
||||||
|
e2eMutateForgejoURL, e2eMutateOwner, e2eMutateRepo, e2eMutateBranch, e2eMutateSHA = originalURL, originalOwner, originalRepo, originalBranch, originalSHA
|
||||||
|
e2eMutateTokenEnv, e2eMutateTokenFile, e2eMutateOpenPR = originalEnv, originalFile, originalOpenPR
|
||||||
|
})
|
||||||
|
|
||||||
|
command, _, err := rootCmd.Find([]string{"e2e-mutate"})
|
||||||
|
if err != nil || command != e2eMutateCmd || command.Flags().Lookup("token") != nil {
|
||||||
|
t.Fatalf("e2e-mutate command or token flags are not wired safely: %v", err)
|
||||||
|
}
|
||||||
|
fake := &commandMutationHTTP{}
|
||||||
|
e2eMutator = func() e2emutate.Mutator { return e2emutate.Mutator{HTTP: fake} }
|
||||||
|
e2eMutateForgejoURL, e2eMutateOwner = "https://git.example.test", "test-org-2"
|
||||||
|
e2eMutateRepo, e2eMutateBranch = "maidn-e2e-repo", "maidn-e2e-branch"
|
||||||
|
e2eMutateSHA = "0123456789abcdef0123456789abcdef01234567"
|
||||||
|
e2eMutateTokenEnv, e2eMutateTokenFile, e2eMutateOpenPR = "E2E_MUTATE_TEST_TOKEN", "", false
|
||||||
|
t.Setenv(e2eMutateTokenEnv, "test-token")
|
||||||
|
if err := runE2EMutate(&cobra.Command{}, nil); err != nil || len(fake.calls) != 1 || fake.calls[0].Method != http.MethodPatch {
|
||||||
|
t.Fatalf("runE2EMutate() = %v, calls = %#v", err, fake.calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
232
internal/e2emutate/mutate.go
Normal file
232
internal/e2emutate/mutate.go
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
// Package e2emutate contains narrowly scoped Forgejo fixture mutations.
|
||||||
|
package e2emutate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fixtureOwner = "test-org-2"
|
||||||
|
fixturePrefix = "maidn-e2e-"
|
||||||
|
maxBodyBytes = 1 << 20
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
forgejoName = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`)
|
||||||
|
gitSHA = regexp.MustCompile(`^[0-9a-fA-F]{40}([0-9a-fA-F]{24})?$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options identifies the only Forgejo resources this command may mutate.
|
||||||
|
type Options struct {
|
||||||
|
ForgejoURL string
|
||||||
|
Owner string
|
||||||
|
Repo string
|
||||||
|
Branch string
|
||||||
|
SHA string
|
||||||
|
Token string
|
||||||
|
OpenPR bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPDoer is the Forgejo API boundary and can be faked in tests.
|
||||||
|
type HTTPDoer interface {
|
||||||
|
Do(*http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutator struct {
|
||||||
|
HTTP HTTPDoer
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadToken(environment, path string) (string, error) {
|
||||||
|
if environment != "" && path != "" {
|
||||||
|
return "", errors.New("use only one Forgejo token reference")
|
||||||
|
}
|
||||||
|
var token string
|
||||||
|
if environment != "" {
|
||||||
|
var present bool
|
||||||
|
token, present = os.LookupEnv(environment)
|
||||||
|
if !present {
|
||||||
|
return "", errors.New("Forgejo token environment variable is not set")
|
||||||
|
}
|
||||||
|
} else if path != "" {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("read Forgejo token file")
|
||||||
|
}
|
||||||
|
token = string(data)
|
||||||
|
} else {
|
||||||
|
return "", errors.New("a Forgejo token environment or file reference is required")
|
||||||
|
}
|
||||||
|
token = strings.TrimSpace(token)
|
||||||
|
if token == "" || strings.ContainsAny(token, "\r\n") {
|
||||||
|
return "", errors.New("Forgejo token reference is empty or invalid")
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Options) Validate() error {
|
||||||
|
if err := validForgejoURL(o.ForgejoURL); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if o.Owner != fixtureOwner {
|
||||||
|
return errors.New("Forgejo mutation owner must be test-org-2")
|
||||||
|
}
|
||||||
|
for _, value := range []string{o.Repo, o.Branch} {
|
||||||
|
if !fixtureName(value) {
|
||||||
|
return errors.New("Forgejo mutation repository and branch must be maidn-e2e fixture identifiers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !gitSHA.MatchString(o.SHA) {
|
||||||
|
return errors.New("Forgejo mutation SHA must be a full Git object ID")
|
||||||
|
}
|
||||||
|
if o.Token == "" || strings.ContainsAny(o.Token, "\r\n") {
|
||||||
|
return errors.New("Forgejo token is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fixtureName(value string) bool {
|
||||||
|
return strings.HasPrefix(value, fixturePrefix) && forgejoName.MatchString(value) && !strings.Contains(value, "..") && !strings.HasSuffix(value, ".") && !strings.HasSuffix(value, ".lock")
|
||||||
|
}
|
||||||
|
|
||||||
|
func validForgejoURL(value string) error {
|
||||||
|
parsed, err := url.Parse(value)
|
||||||
|
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" {
|
||||||
|
return errors.New("Forgejo URL must be a credential-free HTTP(S) URL without query or fragment")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run updates one fixture branch and may ensure its single PR to main.
|
||||||
|
func (m Mutator) Run(ctx context.Context, options Options) error {
|
||||||
|
if err := options.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if m.HTTP == nil {
|
||||||
|
return errors.New("Forgejo mutation API is required")
|
||||||
|
}
|
||||||
|
if err := m.updateRef(ctx, options); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if options.OpenPR {
|
||||||
|
return m.ensurePR(ctx, options)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Mutator) updateRef(ctx context.Context, options Options) error {
|
||||||
|
endpoint := options.apiURL("git", "refs", "heads", options.Branch)
|
||||||
|
status, err := m.request(ctx, options, http.MethodPatch, endpoint, struct {
|
||||||
|
SHA string `json:"sha"`
|
||||||
|
Force bool `json:"force"`
|
||||||
|
}{SHA: options.SHA})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if status >= http.StatusOK && status < http.StatusMultipleChoices {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if status != http.StatusNotFound {
|
||||||
|
return errors.New("Forgejo branch update failed")
|
||||||
|
}
|
||||||
|
status, err = m.request(ctx, options, http.MethodPost, options.apiURL("git", "refs"), struct {
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
SHA string `json:"sha"`
|
||||||
|
}{Ref: "refs/heads/" + options.Branch, SHA: options.SHA})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if status < http.StatusOK || status >= http.StatusMultipleChoices {
|
||||||
|
return errors.New("Forgejo branch creation failed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Mutator) ensurePR(ctx context.Context, options Options) error {
|
||||||
|
endpoint, _ := url.Parse(options.apiURL("pulls"))
|
||||||
|
endpoint.RawQuery = url.Values{"state": {"open"}, "head": {options.Owner + ":" + options.Branch}}.Encode()
|
||||||
|
request, err := m.newRequest(ctx, options, http.MethodGet, endpoint.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
response, err := m.HTTP.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("Forgejo mutation request failed")
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return errors.New("Forgejo pull request lookup failed")
|
||||||
|
}
|
||||||
|
var pulls []json.RawMessage
|
||||||
|
if json.NewDecoder(io.LimitReader(response.Body, maxBodyBytes)).Decode(&pulls) != nil {
|
||||||
|
return errors.New("Forgejo pull request lookup returned invalid data")
|
||||||
|
}
|
||||||
|
if len(pulls) > 1 {
|
||||||
|
return errors.New("multiple open Forgejo pull requests exist for the fixture branch")
|
||||||
|
}
|
||||||
|
if len(pulls) == 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
status, err := m.request(ctx, options, http.MethodPost, options.apiURL("pulls"), struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Head string `json:"head"`
|
||||||
|
Base string `json:"base"`
|
||||||
|
}{Title: "maidn e2e mutation", Head: options.Branch, Base: "main"})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if status < http.StatusOK || status >= http.StatusMultipleChoices {
|
||||||
|
return errors.New("Forgejo pull request creation failed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Mutator) request(ctx context.Context, options Options, method, endpoint string, body any) (int, error) {
|
||||||
|
request, err := m.newRequest(ctx, options, method, endpoint, body)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
response, err := m.HTTP.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.New("Forgejo mutation request failed")
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
return response.StatusCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Mutator) newRequest(ctx context.Context, options Options, method, endpoint string, body any) (*http.Request, error) {
|
||||||
|
var reader io.Reader
|
||||||
|
if body != nil {
|
||||||
|
data, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("encode Forgejo mutation request")
|
||||||
|
}
|
||||||
|
reader = bytes.NewReader(data)
|
||||||
|
}
|
||||||
|
request, err := http.NewRequestWithContext(ctx, method, endpoint, reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("create Forgejo mutation request")
|
||||||
|
}
|
||||||
|
request.Header.Set("Authorization", "token "+options.Token)
|
||||||
|
if body != nil {
|
||||||
|
request.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
return request, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Options) apiURL(parts ...string) string {
|
||||||
|
return strings.TrimRight(o.ForgejoURL, "/") + "/api/v1/repos/" + o.Owner + "/" + o.Repo + "/" + strings.Join(parts, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultMutator() Mutator {
|
||||||
|
return Mutator{HTTP: &http.Client{Timeout: 15 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}}
|
||||||
|
}
|
||||||
138
internal/e2emutate/mutate_test.go
Normal file
138
internal/e2emutate/mutate_test.go
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
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: "test-org-2",
|
||||||
|
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/test-org-2/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/test-org-2/maidn-e2e-repo/pulls":
|
||||||
|
if request.URL.Query().Get("head") != "test-org-2: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/test-org-2/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/test-org-2/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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue