package forgejo import ( "encoding/json" "errors" "net/http" "net/http/httptest" "reflect" "strings" "testing" ) func TestCreateRegistryTokenUsesBasicAuthAndPackageScopes(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if request.Method != http.MethodPost || request.URL.Path != "/api/v1/users/registry-user/tokens" { t.Fatal("unexpected Forgejo token request") } username, password, ok := request.BasicAuth() if !ok || username != "registry-user" || password != "password" { t.Fatal("Forgejo token request did not use the supplied basic authentication") } if request.Header.Get("X-Forgejo-OTP") != "123456" || request.Header.Get("Content-Type") != "application/json" { t.Fatal("Forgejo token request headers are incorrect") } var body createTokenRequest if err := json.NewDecoder(request.Body).Decode(&body); err != nil { t.Fatal(err) } if body.Name != "maidn-registry" || !reflect.DeepEqual(body.Scopes, []string{"read:package", "write:package"}) { t.Fatal("Forgejo token request did not use the least-privilege package scopes") } writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusCreated) _, _ = writer.Write([]byte(`{"sha1":"registry-token"}`)) })) defer server.Close() token, err := createRegistryToken(server.Client(), server.URL, "registry-user", "password", "123456", "maidn-registry") if err != nil || token != "registry-token" { t.Fatal("CreateRegistryToken() did not return the Forgejo token") } } func TestEnsureRepositoryCopyUsesAskPassAndCredentialFreeGitArguments(t *testing.T) { original := copyGit t.Cleanup(func() { copyGit = original }) var commands []string copyGit = func(_ string, _ []string, args ...string) error { commands = append(commands, strings.Join(args, " ")) return nil } server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { switch request.Method { case http.MethodGet: if request.URL.Path != "/api/v1/repos/owner/catalog" { t.Fatalf("unexpected lookup %q", request.URL.Path) } writer.WriteHeader(http.StatusNotFound) case http.MethodPost: if request.URL.Path != "/api/v1/orgs/owner/repos" { t.Fatalf("unexpected create %q", request.URL.Path) } var body createRepoRequest if err := json.NewDecoder(request.Body).Decode(&body); err != nil || body.AutoInit || body.DefaultBranch != "release" { t.Fatalf("unexpected catalog create request: %#v, %v", body, err) } writer.WriteHeader(http.StatusCreated) default: t.Fatalf("unexpected request method %q", request.Method) } })) defer server.Close() manager := NewRepoManager(server.URL, "test-token", "owner", "bot", "", "", "release", "") manager.HTTPClient = server.Client() if created, err := manager.EnsureRepositoryCopy("catalog", "catalog", "https://catalog.example.test/tekton.git"); err != nil || !created { t.Fatalf("EnsureRepositoryCopy() = (%t, %v)", created, err) } joined := strings.Join(commands, "\n") if !strings.Contains(joined, "clone --mirror https://catalog.example.test/tekton.git") || !strings.Contains(joined, "push --mirror "+CloneURL(server.URL, "owner", "catalog")) || !strings.Contains(joined, "ls-remote --exit-code "+CloneURL(server.URL, "owner", "catalog")+" refs/heads/release") || strings.Contains(joined, "test-token") { t.Fatalf("catalog copy command boundary is unsafe: %q", joined) } } func TestEnsureRepositoryCopyRejectsMissingConfiguredRefAfterCopy(t *testing.T) { original := copyGit t.Cleanup(func() { copyGit = original }) var commands []string copyGit = func(_ string, _ []string, args ...string) error { commands = append(commands, strings.Join(args, " ")) if args[0] == "ls-remote" { return errors.New("missing ref") } return nil } server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { switch request.Method { case http.MethodGet: writer.WriteHeader(http.StatusNotFound) case http.MethodPost: writer.WriteHeader(http.StatusCreated) default: t.Fatalf("unexpected request method %q", request.Method) } })) defer server.Close() manager := NewRepoManager(server.URL, "test-token", "owner", "bot", "", "", "release", "") manager.HTTPClient = server.Client() if created, err := manager.EnsureRepositoryCopy("catalog", "catalog", "https://catalog.example.test/tekton.git"); !created || err == nil || !strings.Contains(err.Error(), "configured catalog ref") { t.Fatalf("EnsureRepositoryCopy() = (%t, %v), want copied catalog ref error", created, err) } joined := strings.Join(commands, "\n") if !strings.Contains(joined, "push --mirror "+CloneURL(server.URL, "owner", "catalog")) || !strings.Contains(joined, "ls-remote --exit-code "+CloneURL(server.URL, "owner", "catalog")+" refs/heads/release") { t.Fatalf("catalog copy did not verify the missing non-default ref: %q", joined) } } func TestEnsureRepositoryCopyRejectsCredentialBearingSourceBeforeHTTP(t *testing.T) { manager := NewRepoManager("https://git.example.test", "test-token", "owner", "bot", "", "", "main", "") manager.HTTPClient = &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { t.Fatal("credential-bearing source reached Forgejo HTTP") return nil, nil })} if _, err := manager.EnsureRepositoryCopy("catalog", "catalog", "https://reader:secret@catalog.example.test/tekton.git"); err == nil || strings.Contains(err.Error(), "secret") { t.Fatalf("EnsureRepositoryCopy() error = %v", err) } } type roundTripFunc func(*http.Request) (*http.Response, error) func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { return f(request) } func TestRepoExistsOnlyCreatesOnNotFound(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusUnauthorized) })) defer server.Close() manager := NewRepoManager(server.URL, "token", "owner", "user", "manifests", "cluster", "main", "maidn/bootstrap-test") manager.HTTPClient = server.Client() if _, err := manager.repoExists("cluster"); err == nil { t.Fatal("repoExists() accepted an unauthorized response") } } func TestEnsureWebhookUpdatesMatchingURL(t *testing.T) { requests := 0 server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { requests++ switch request.Method { case http.MethodGet: if request.URL.Path != "/api/v1/repos/owner/app/hooks" { t.Fatalf("unexpected lookup path %q", request.URL.Path) } _ = json.NewEncoder(writer).Encode([]hook{{ID: 7, URL: "https://tekton.example.test/"}}) case http.MethodPatch: if request.URL.Path != "/api/v1/repos/owner/app/hooks/7" { t.Fatalf("unexpected update path %q", request.URL.Path) } var body hookRequest if err := json.NewDecoder(request.Body).Decode(&body); err != nil { t.Fatal(err) } if body.Type != "" || body.AuthorizationHeader != "Bearer token" || len(body.Events) != 2 { t.Fatalf("unexpected hook request: %#v", body) } writer.WriteHeader(http.StatusOK) default: t.Fatalf("unexpected method %q", request.Method) } })) defer server.Close() manager := NewRepoManager(server.URL, "token", "owner", "user", "manifests", "cluster", "main", "maidn/bootstrap-test") manager.HTTPClient = server.Client() if err := manager.EnsureWebhook("app", "https://tekton.example.test/", "Bearer token"); err != nil { t.Fatal(err) } if requests != 2 { t.Fatalf("requests = %d, want 2", requests) } } func TestEnsureWebhookCreatesMissingWebhook(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { switch request.Method { case http.MethodGet: _ = json.NewEncoder(writer).Encode([]hook{}) case http.MethodPost: var body hookRequest if err := json.NewDecoder(request.Body).Decode(&body); err != nil { t.Fatal(err) } if body.Type != "forgejo" || body.Config["content_type"] != "json" { t.Fatalf("unexpected hook request: %#v", body) } writer.WriteHeader(http.StatusCreated) default: t.Fatalf("unexpected method %q", request.Method) } })) defer server.Close() manager := NewRepoManager(server.URL, "token", "owner", "user", "manifests", "cluster", "main", "maidn/bootstrap-test") manager.HTTPClient = server.Client() if err := manager.EnsureWebhook("app", "https://tekton.example.test/", "Bearer token"); err != nil { t.Fatal(err) } } func TestEnsureProtectedBranchCreatesDirectPushProtection(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if request.URL.Path != "/api/v1/repos/owner/app/branch_protections" { t.Fatalf("unexpected branch protection path %q", request.URL.Path) } switch request.Method { case http.MethodGet: _ = json.NewEncoder(writer).Encode([]branchProtection{}) case http.MethodPost: var body branchProtectionRequest if err := json.NewDecoder(request.Body).Decode(&body); err != nil { t.Fatal(err) } if body.BranchName != "production" || body.EnablePush || body.EnablePushWhitelist { t.Fatalf("unexpected branch protection request: %#v", body) } writer.WriteHeader(http.StatusCreated) default: t.Fatalf("unexpected method %q", request.Method) } })) defer server.Close() manager := NewRepoManager(server.URL, "token", "owner", "user", "", "", "main", "") manager.HTTPClient = server.Client() if err := manager.EnsureProtectedBranch("app", "production"); err != nil { t.Fatal(err) } } func TestEnsureProtectedBranchRejectsExistingDirectPushRule(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if request.Method != http.MethodGet { t.Fatal("existing unsafe protection must not be changed") } _ = json.NewEncoder(writer).Encode([]branchProtection{{ID: 1, BranchName: "production", EnablePush: true}}) })) defer server.Close() manager := NewRepoManager(server.URL, "token", "owner", "user", "", "", "main", "") manager.HTTPClient = server.Client() if err := manager.EnsureProtectedBranch("app", "production"); err == nil || !strings.Contains(err.Error(), "permits direct pushes") { t.Fatalf("EnsureProtectedBranch() error = %v, want unsafe rule error", err) } } func TestRepositoryFromURL(t *testing.T) { owner, repo, err := RepositoryFromURL("https://git.example.test/team/app.git") if err != nil || owner != "team" || repo != "app" { t.Fatalf("RepositoryFromURL() = (%q, %q, %v)", owner, repo, err) } } func TestEnsureRepositoryCreatesWithoutInitialization(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { switch request.Method { case http.MethodGet: writer.WriteHeader(http.StatusNotFound) case http.MethodPost: var body createRepoRequest if err := json.NewDecoder(request.Body).Decode(&body); err != nil { t.Fatal(err) } if body.AutoInit { t.Fatal("application repository must be created without an initial commit") } writer.WriteHeader(http.StatusCreated) default: t.Fatalf("unexpected method %q", request.Method) } })) defer server.Close() manager := NewRepoManager(server.URL, "token", "owner", "user", "manifests", "cluster", "main", "maidn/bootstrap-test") manager.HTTPClient = server.Client() created, err := manager.EnsureRepository("app", "description") if err != nil || !created { t.Fatalf("EnsureRepository() = (%t, %v)", created, err) } } func TestHasRemoteBranchReturnsFalseForMissingBranch(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusNotFound) })) defer server.Close() manager := NewRepoManager(server.URL, "token", "owner", "user", "manifests", "cluster", "main", "maidn/bootstrap-test") manager.HTTPClient = server.Client() if _, err := manager.HasRemoteBranch(server.URL, "main"); err == nil { t.Fatal("HasRemoteBranch() accepted a failed git remote lookup") } } func TestMergePullRequest(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { switch request.Method { case http.MethodGet: _ = json.NewEncoder(writer).Encode([]pullRequest{{Number: 4}}) case http.MethodPost: var body mergePullRequestRequest if err := json.NewDecoder(request.Body).Decode(&body); err != nil { t.Fatal(err) } if body.Do != "merge" || request.URL.Path != "/api/v1/repos/owner/cluster/pulls/4/merge" { t.Fatalf("unexpected merge request") } writer.WriteHeader(http.StatusOK) default: t.Fatalf("unexpected method %q", request.Method) } })) defer server.Close() manager := NewRepoManager(server.URL, "token", "owner", "user", "manifests", "cluster", "main", "maidn/bootstrap-test") manager.HTTPClient = server.Client() if err := manager.MergePullRequest("cluster", "maidn/bootstrap-test"); err != nil { t.Fatal(err) } } func TestRepoExistsReturnsFalseOnNotFound(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusNotFound) })) defer server.Close() manager := NewRepoManager(server.URL, "token", "owner", "user", "manifests", "cluster", "main", "maidn/bootstrap-test") manager.HTTPClient = server.Client() exists, err := manager.repoExists("cluster") if err != nil || exists { t.Fatalf("repoExists() = (%t, %v), want (false, nil)", exists, err) } }