1
0
Fork 0
mirror of https://github.com/imjasonh/git-k8s synced 2026-07-13 00:48:34 +00:00

Add comprehensive e2e tests for repo-watcher-controller

Tests cover the key behaviors of the repo-watcher:
- Branch discovery: auto-creates GitBranch CRDs from remote refs
- Owner references: created branches correctly reference GitRepository
- New branch detection: branches added on Gitea are picked up
- Commit update detection: new pushes to Gitea update headCommit
- Branch deletion: branches removed from Gitea trigger CRD deletion
- Multiple branches: all branches on a repo are discovered
- LastFetchTime: status is updated and advances on each poll
- Labels: repository label is set on created branches
- Slash branch names: feature/foo naming handled correctly

Also adds shared e2e helpers for Gitea branch/file operations and
updates CI workflow to deploy and monitor the repo-watcher-controller.

https://claude.ai/code/session_01P7xfBqARU5DFS8QJ4uDPVj
This commit is contained in:
Claude 2026-02-28 14:15:28 +00:00
parent ee281430b6
commit 6f803bf10e
No known key found for this signature in database
3 changed files with 547 additions and 2 deletions

View file

@ -221,6 +221,173 @@ func createGitBranch(t *testing.T, name, repoRef, branchName string) *gitv1alpha
return created
}
// createGiteaBranch creates a new branch on Gitea from an existing branch.
func createGiteaBranch(t *testing.T, repo, newBranch, fromBranch string) {
t.Helper()
body, _ := json.Marshal(map[string]interface{}{
"new_branch_name": newBranch,
"old_branch_name": fromBranch,
})
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/branches", giteaURL, giteaUsername, repo)
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
if err != nil {
t.Fatalf("creating request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth(giteaUsername, giteaPassword)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("creating Gitea branch %q: %v", newBranch, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
t.Fatalf("creating Gitea branch %q: status %d", newBranch, resp.StatusCode)
}
t.Logf("Created Gitea branch %q on repo %q", newBranch, repo)
}
// deleteGiteaBranch deletes a branch on Gitea.
func deleteGiteaBranch(t *testing.T, repo, branch string) {
t.Helper()
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/branches/%s", giteaURL, giteaUsername, repo, branch)
req, _ := http.NewRequest("DELETE", url, nil)
req.SetBasicAuth(giteaUsername, giteaPassword)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("deleting Gitea branch %q: %v", branch, err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
t.Fatalf("deleting Gitea branch %q: status %d", branch, resp.StatusCode)
}
t.Logf("Deleted Gitea branch %q on repo %q", branch, repo)
}
// createGiteaFile creates a file on Gitea, producing a new commit on the given branch.
// Returns the new commit SHA.
func createGiteaFile(t *testing.T, repo, branch, filePath, content string) string {
t.Helper()
body, _ := json.Marshal(map[string]interface{}{
"content": content,
"branch": branch,
"new_branch": branch,
"message": fmt.Sprintf("add %s", filePath),
})
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/contents/%s", giteaURL, giteaUsername, repo, filePath)
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
if err != nil {
t.Fatalf("creating request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth(giteaUsername, giteaPassword)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("creating file %q: %v", filePath, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
t.Fatalf("creating file %q: status %d", filePath, resp.StatusCode)
}
var result struct {
Commit struct {
SHA string `json:"sha"`
} `json:"commit"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("decoding create file response: %v", err)
}
t.Logf("Created file %q on %s/%s, commit: %s", filePath, repo, branch, result.Commit.SHA)
return result.Commit.SHA
}
// createGitRepositoryWithPollInterval creates a GitRepository CR with a custom poll interval.
func createGitRepositoryWithPollInterval(t *testing.T, name, repoName, secretName string, interval metav1.Duration) *gitv1alpha1.GitRepository {
t.Helper()
ctx := context.Background()
repo := &gitv1alpha1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: testNamespace,
},
Spec: gitv1alpha1.GitRepositorySpec{
URL: fmt.Sprintf("%s/%s/%s.git", giteaInURL, giteaUsername, repoName),
DefaultBranch: "main",
Auth: &gitv1alpha1.GitAuth{
SecretRef: &gitv1alpha1.SecretRef{Name: secretName},
},
PollInterval: &interval,
},
}
created, err := gitClient.GitRepositories(testNamespace).Create(ctx, repo, metav1.CreateOptions{})
if err != nil {
t.Fatalf("creating GitRepository %q: %v", name, err)
}
t.Cleanup(func() {
gitClient.GitRepositories(testNamespace).Delete(ctx, name, metav1.DeleteOptions{})
})
return created
}
// waitForGitBranchExists polls until a GitBranch with the given name exists.
func waitForGitBranchExists(t *testing.T, name string) *gitv1alpha1.GitBranch {
t.Helper()
ctx := context.Background()
deadline := time.Now().Add(pollTimeout)
for time.Now().Before(deadline) {
branch, err := gitClient.GitBranches(testNamespace).Get(ctx, name, metav1.GetOptions{})
if err == nil {
return branch
}
time.Sleep(pollInterval)
}
t.Fatalf("timed out waiting for GitBranch %q to exist", name)
return nil
}
// waitForGitBranchDeleted polls until a GitBranch with the given name no longer exists.
func waitForGitBranchDeleted(t *testing.T, name string) {
t.Helper()
ctx := context.Background()
deadline := time.Now().Add(pollTimeout)
for time.Now().Before(deadline) {
_, err := gitClient.GitBranches(testNamespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
t.Logf("GitBranch %q deleted", name)
return
}
time.Sleep(pollInterval)
}
t.Fatalf("timed out waiting for GitBranch %q to be deleted", name)
}
// waitForGitBranchHeadCommit polls until the GitBranch has the expected headCommit.
func waitForGitBranchHeadCommit(t *testing.T, name, expectedCommit string) {
t.Helper()
ctx := context.Background()
deadline := time.Now().Add(pollTimeout)
for time.Now().Before(deadline) {
branch, err := gitClient.GitBranches(testNamespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
time.Sleep(pollInterval)
continue
}
if branch.Status.HeadCommit == expectedCommit {
t.Logf("GitBranch %q headCommit matches: %s", name, expectedCommit)
return
}
t.Logf("GitBranch %q headCommit = %q, waiting for %q", name, branch.Status.HeadCommit, expectedCommit)
time.Sleep(pollInterval)
}
t.Fatalf("timed out waiting for GitBranch %q to have headCommit %q", name, expectedCommit)
}
// waitForPushTransaction polls until the GitPushTransaction reaches a terminal phase.
func waitForPushTransaction(t *testing.T, name string) *gitv1alpha1.GitPushTransaction {
t.Helper()

View file

@ -0,0 +1,377 @@
//go:build e2e
package e2e
import (
"context"
"encoding/base64"
"fmt"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// TestRepoWatcherDiscoversBranches verifies that when a GitRepository is created
// pointing at a Gitea repo with a "main" branch, the repo-watcher controller
// auto-creates a GitBranch CRD with the correct headCommit.
func TestRepoWatcherDiscoversBranches(t *testing.T) {
t.Parallel()
repoName := "e2e-watcher-discover"
createGiteaRepo(t, repoName)
secretName := createCredentialsSecret(t, "e2e-watcher-discover-creds")
// Use a short poll interval so we don't wait long.
interval := metav1.Duration{Duration: 2 * time.Second}
repo := createGitRepositoryWithPollInterval(t, "e2e-watcher-discover-repo", repoName, secretName, interval)
_ = repo
// The repo-watcher should auto-create a GitBranch for "main".
branchCRDName := "e2e-watcher-discover-repo-main"
branch := waitForGitBranchExists(t, branchCRDName)
// Verify the branch spec.
if branch.Spec.RepositoryRef != "e2e-watcher-discover-repo" {
t.Errorf("GitBranch repositoryRef = %q, want %q", branch.Spec.RepositoryRef, "e2e-watcher-discover-repo")
}
if branch.Spec.BranchName != "main" {
t.Errorf("GitBranch branchName = %q, want %q", branch.Spec.BranchName, "main")
}
// Verify headCommit matches what Gitea reports.
expectedCommit := getGiteaBranchCommit(t, repoName, "main")
commit := waitForGitBranchCommit(t, branchCRDName)
if commit != expectedCommit {
t.Errorf("GitBranch headCommit = %q, want %q", commit, expectedCommit)
}
// Cleanup: the watcher-created branch won't be cleaned up by our test helpers
// since we didn't create it. Delete it explicitly.
t.Cleanup(func() {
gitClient.GitBranches(testNamespace).Delete(context.Background(), branchCRDName, metav1.DeleteOptions{})
})
}
// TestRepoWatcherOwnerReferences verifies that auto-created GitBranch CRDs have
// correct owner references pointing back to the GitRepository.
func TestRepoWatcherOwnerReferences(t *testing.T) {
t.Parallel()
repoName := "e2e-watcher-ownerref"
createGiteaRepo(t, repoName)
secretName := createCredentialsSecret(t, "e2e-watcher-ownerref-creds")
interval := metav1.Duration{Duration: 2 * time.Second}
repo := createGitRepositoryWithPollInterval(t, "e2e-watcher-ownerref-repo", repoName, secretName, interval)
branchCRDName := "e2e-watcher-ownerref-repo-main"
branch := waitForGitBranchExists(t, branchCRDName)
// Verify owner reference.
if len(branch.OwnerReferences) == 0 {
t.Fatal("GitBranch has no owner references")
}
ownerRef := branch.OwnerReferences[0]
if ownerRef.Kind != "GitRepository" {
t.Errorf("owner reference kind = %q, want %q", ownerRef.Kind, "GitRepository")
}
if ownerRef.Name != "e2e-watcher-ownerref-repo" {
t.Errorf("owner reference name = %q, want %q", ownerRef.Name, "e2e-watcher-ownerref-repo")
}
if ownerRef.UID != repo.UID {
t.Errorf("owner reference UID = %q, want %q", ownerRef.UID, repo.UID)
}
t.Cleanup(func() {
gitClient.GitBranches(testNamespace).Delete(context.Background(), branchCRDName, metav1.DeleteOptions{})
})
}
// TestRepoWatcherDetectsNewBranch verifies that when a new branch is created on
// Gitea, the repo-watcher creates a corresponding GitBranch CRD.
func TestRepoWatcherDetectsNewBranch(t *testing.T) {
t.Parallel()
repoName := "e2e-watcher-newbranch"
createGiteaRepo(t, repoName)
secretName := createCredentialsSecret(t, "e2e-watcher-newbranch-creds")
interval := metav1.Duration{Duration: 2 * time.Second}
createGitRepositoryWithPollInterval(t, "e2e-watcher-newbranch-repo", repoName, secretName, interval)
// Wait for the initial "main" branch to be discovered.
mainBranchCRD := "e2e-watcher-newbranch-repo-main"
waitForGitBranchExists(t, mainBranchCRD)
t.Cleanup(func() {
gitClient.GitBranches(testNamespace).Delete(context.Background(), mainBranchCRD, metav1.DeleteOptions{})
})
// Create a new branch on Gitea.
createGiteaBranch(t, repoName, "feature-new", "main")
// The watcher should detect the new branch.
featureBranchCRD := "e2e-watcher-newbranch-repo-feature-new"
branch := waitForGitBranchExists(t, featureBranchCRD)
if branch.Spec.BranchName != "feature-new" {
t.Errorf("GitBranch branchName = %q, want %q", branch.Spec.BranchName, "feature-new")
}
// The feature branch should have the same commit as main (branched from it).
expectedCommit := getGiteaBranchCommit(t, repoName, "feature-new")
commit := waitForGitBranchCommit(t, featureBranchCRD)
if commit != expectedCommit {
t.Errorf("GitBranch headCommit = %q, want %q", commit, expectedCommit)
}
t.Cleanup(func() {
gitClient.GitBranches(testNamespace).Delete(context.Background(), featureBranchCRD, metav1.DeleteOptions{})
})
}
// TestRepoWatcherDetectsCommitUpdate verifies that when a new commit is pushed to
// a branch on Gitea, the repo-watcher updates the GitBranch headCommit.
func TestRepoWatcherDetectsCommitUpdate(t *testing.T) {
t.Parallel()
repoName := "e2e-watcher-commit"
createGiteaRepo(t, repoName)
secretName := createCredentialsSecret(t, "e2e-watcher-commit-creds")
interval := metav1.Duration{Duration: 2 * time.Second}
createGitRepositoryWithPollInterval(t, "e2e-watcher-commit-repo", repoName, secretName, interval)
// Wait for initial discovery.
branchCRDName := "e2e-watcher-commit-repo-main"
waitForGitBranchExists(t, branchCRDName)
oldCommit := waitForGitBranchCommit(t, branchCRDName)
t.Logf("Initial headCommit: %s", oldCommit)
// Push a new commit by creating a file on main via Gitea API.
fileContent := base64.StdEncoding.EncodeToString([]byte("hello from e2e test"))
newCommit := createGiteaFile(t, repoName, "main", "test-file.txt", fileContent)
t.Logf("New commit after file creation: %s", newCommit)
// Verify the commit changed on Gitea.
giteaCommit := getGiteaBranchCommit(t, repoName, "main")
if giteaCommit == oldCommit {
t.Fatal("Gitea commit did not change after creating file")
}
// Wait for the watcher to pick up the new commit.
waitForGitBranchHeadCommit(t, branchCRDName, giteaCommit)
t.Cleanup(func() {
gitClient.GitBranches(testNamespace).Delete(context.Background(), branchCRDName, metav1.DeleteOptions{})
})
}
// TestRepoWatcherDeletesBranch verifies that when a branch is deleted on Gitea,
// the repo-watcher deletes the corresponding GitBranch CRD.
func TestRepoWatcherDeletesBranch(t *testing.T) {
t.Parallel()
repoName := "e2e-watcher-delete"
createGiteaRepo(t, repoName)
secretName := createCredentialsSecret(t, "e2e-watcher-delete-creds")
interval := metav1.Duration{Duration: 2 * time.Second}
createGitRepositoryWithPollInterval(t, "e2e-watcher-delete-repo", repoName, secretName, interval)
// Create a feature branch on Gitea.
createGiteaBranch(t, repoName, "to-delete", "main")
// Wait for both branches to be discovered.
mainCRD := "e2e-watcher-delete-repo-main"
featureCRD := "e2e-watcher-delete-repo-to-delete"
waitForGitBranchExists(t, mainCRD)
waitForGitBranchExists(t, featureCRD)
t.Logf("Both branches discovered")
// Delete the feature branch on Gitea.
deleteGiteaBranch(t, repoName, "to-delete")
// Wait for the watcher to delete the GitBranch CRD.
waitForGitBranchDeleted(t, featureCRD)
// Main should still exist.
ctx := context.Background()
_, err := gitClient.GitBranches(testNamespace).Get(ctx, mainCRD, metav1.GetOptions{})
if err != nil {
t.Errorf("main GitBranch should still exist, but got error: %v", err)
}
t.Cleanup(func() {
gitClient.GitBranches(testNamespace).Delete(ctx, mainCRD, metav1.DeleteOptions{})
})
}
// TestRepoWatcherMultipleBranches verifies that the watcher discovers all branches
// on a repository, not just the default one.
func TestRepoWatcherMultipleBranches(t *testing.T) {
t.Parallel()
repoName := "e2e-watcher-multi"
createGiteaRepo(t, repoName)
// Create several branches on Gitea before creating the GitRepository CR.
createGiteaBranch(t, repoName, "develop", "main")
createGiteaBranch(t, repoName, "release-v1", "main")
createGiteaBranch(t, repoName, "hotfix", "main")
secretName := createCredentialsSecret(t, "e2e-watcher-multi-creds")
interval := metav1.Duration{Duration: 2 * time.Second}
createGitRepositoryWithPollInterval(t, "e2e-watcher-multi-repo", repoName, secretName, interval)
// All four branches should be discovered.
expectedBranches := map[string]string{
"e2e-watcher-multi-repo-main": "main",
"e2e-watcher-multi-repo-develop": "develop",
"e2e-watcher-multi-repo-release-v1": "release-v1",
"e2e-watcher-multi-repo-hotfix": "hotfix",
}
ctx := context.Background()
for crdName, branchName := range expectedBranches {
branch := waitForGitBranchExists(t, crdName)
if branch.Spec.BranchName != branchName {
t.Errorf("GitBranch %q has branchName %q, want %q", crdName, branch.Spec.BranchName, branchName)
}
commit := waitForGitBranchCommit(t, crdName)
if commit == "" {
t.Errorf("GitBranch %q has empty headCommit", crdName)
}
t.Cleanup(func() {
gitClient.GitBranches(testNamespace).Delete(ctx, crdName, metav1.DeleteOptions{})
})
}
t.Logf("All %d branches discovered", len(expectedBranches))
}
// TestRepoWatcherLastFetchTime verifies that the controller updates
// GitRepository.status.lastFetchTime after each poll.
func TestRepoWatcherLastFetchTime(t *testing.T) {
t.Parallel()
repoName := "e2e-watcher-fetchtime"
createGiteaRepo(t, repoName)
secretName := createCredentialsSecret(t, "e2e-watcher-fetchtime-creds")
interval := metav1.Duration{Duration: 2 * time.Second}
createGitRepositoryWithPollInterval(t, "e2e-watcher-fetchtime-repo", repoName, secretName, interval)
// Wait for the branch to be discovered (proves at least one poll happened).
mainCRD := "e2e-watcher-fetchtime-repo-main"
waitForGitBranchExists(t, mainCRD)
// Check that lastFetchTime is set on the GitRepository.
ctx := context.Background()
deadline := time.Now().Add(pollTimeout)
for time.Now().Before(deadline) {
repo, err := gitClient.GitRepositories(testNamespace).Get(ctx, "e2e-watcher-fetchtime-repo", metav1.GetOptions{})
if err != nil {
time.Sleep(pollInterval)
continue
}
if repo.Status.LastFetchTime != nil {
t.Logf("GitRepository lastFetchTime: %v", repo.Status.LastFetchTime.Time)
// Wait a few polls and check that it advances.
firstFetch := repo.Status.LastFetchTime.Time
time.Sleep(5 * time.Second)
repo2, err := gitClient.GitRepositories(testNamespace).Get(ctx, "e2e-watcher-fetchtime-repo", metav1.GetOptions{})
if err != nil {
t.Fatalf("getting GitRepository: %v", err)
}
if repo2.Status.LastFetchTime == nil {
t.Fatal("lastFetchTime is nil after second check")
}
if !repo2.Status.LastFetchTime.Time.After(firstFetch) {
t.Errorf("lastFetchTime did not advance: first=%v second=%v", firstFetch, repo2.Status.LastFetchTime.Time)
}
t.Logf("lastFetchTime advanced from %v to %v", firstFetch, repo2.Status.LastFetchTime.Time)
t.Cleanup(func() {
gitClient.GitBranches(testNamespace).Delete(context.Background(), mainCRD, metav1.DeleteOptions{})
})
return
}
time.Sleep(pollInterval)
}
t.Fatal("timed out waiting for lastFetchTime to be set")
}
// TestRepoWatcherLabels verifies that auto-created GitBranch CRDs have the
// repository label set correctly.
func TestRepoWatcherLabels(t *testing.T) {
t.Parallel()
repoName := "e2e-watcher-labels"
createGiteaRepo(t, repoName)
secretName := createCredentialsSecret(t, "e2e-watcher-labels-creds")
interval := metav1.Duration{Duration: 2 * time.Second}
createGitRepositoryWithPollInterval(t, "e2e-watcher-labels-repo", repoName, secretName, interval)
branchCRDName := "e2e-watcher-labels-repo-main"
branch := waitForGitBranchExists(t, branchCRDName)
// Verify label.
expectedLabel := "git-k8s.imjasonh.com/repository"
labelValue, ok := branch.Labels[expectedLabel]
if !ok {
t.Fatalf("GitBranch missing label %q, labels: %v", expectedLabel, branch.Labels)
}
if labelValue != "e2e-watcher-labels-repo" {
t.Errorf("label %q = %q, want %q", expectedLabel, labelValue, "e2e-watcher-labels-repo")
}
t.Cleanup(func() {
gitClient.GitBranches(testNamespace).Delete(context.Background(), branchCRDName, metav1.DeleteOptions{})
})
}
// TestRepoWatcherSlashBranchName verifies that branches with slashes in the name
// (e.g., "feature/foo") are handled correctly, with slashes replaced by dashes
// in the CRD name.
func TestRepoWatcherSlashBranchName(t *testing.T) {
t.Parallel()
repoName := "e2e-watcher-slash"
createGiteaRepo(t, repoName)
// Create a branch with a slash in the name.
createGiteaBranch(t, repoName, "feature/my-thing", "main")
secretName := createCredentialsSecret(t, "e2e-watcher-slash-creds")
interval := metav1.Duration{Duration: 2 * time.Second}
createGitRepositoryWithPollInterval(t, "e2e-watcher-slash-repo", repoName, secretName, interval)
// The CRD name should have slashes replaced with dashes.
mainCRD := "e2e-watcher-slash-repo-main"
featureCRD := "e2e-watcher-slash-repo-feature-my-thing"
waitForGitBranchExists(t, mainCRD)
branch := waitForGitBranchExists(t, featureCRD)
// But the spec.branchName should preserve the original slash.
if branch.Spec.BranchName != "feature/my-thing" {
t.Errorf("GitBranch branchName = %q, want %q", branch.Spec.BranchName, "feature/my-thing")
}
expectedCommit := getGiteaBranchCommit(t, repoName, fmt.Sprintf("feature/my-thing"))
commit := waitForGitBranchCommit(t, featureCRD)
if commit != expectedCommit {
t.Errorf("headCommit = %q, want %q", commit, expectedCommit)
}
ctx := context.Background()
t.Cleanup(func() {
gitClient.GitBranches(testNamespace).Delete(ctx, mainCRD, metav1.DeleteOptions{})
gitClient.GitBranches(testNamespace).Delete(ctx, featureCRD, metav1.DeleteOptions{})
})
}