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

Rewrite e2e tests in Go, replace bash-in-YAML test steps

Move all e2e test logic from bash steps in ci.yaml into Go test files
under test/e2e/. CI now sets up infrastructure (KinD, Knative, CRDs,
controllers, Gitea) then runs `go test -tags=e2e ./test/e2e/`.

Benefits:
- Tests run concurrently via t.Parallel()
- No more bash-in-YAML maintenance burden
- Each test creates its own isolated resources with cleanup
- Proper Go test output with -v
- Build-tagged so `go test ./...` skips them

Tests:
- TestPushTransaction: push main to a new branch, verify on git server
- TestPushTransactionMultipleRefSpecs: push to two branches at once
- TestRepoSync: create two repos, set up GitRepoSync, verify controller

https://claude.ai/code/session_01QfFzqKQUsxxBsiZUhuJ3pG
This commit is contained in:
Claude 2026-02-28 02:04:29 +00:00
parent 163e810858
commit 32137c3bc4
No known key found for this signature in database
4 changed files with 509 additions and 250 deletions

291
test/e2e/e2e_test.go Normal file
View file

@ -0,0 +1,291 @@
//go:build e2e
package e2e
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"testing"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
gitv1alpha1 "github.com/imjasonh/git-k8s/pkg/apis/git/v1alpha1"
gitclient "github.com/imjasonh/git-k8s/pkg/client"
)
const (
testNamespace = "default"
giteaUsername = "testadmin"
giteaPassword = "testpassword123"
pollInterval = 2 * time.Second
pollTimeout = 2 * time.Minute
)
var (
gitClient *gitclient.GitV1alpha1Client
kubeClient kubernetes.Interface
giteaURL string // localhost URL for test process (via port-forward)
giteaInURL string // in-cluster URL for controllers
)
func TestMain(m *testing.M) {
// Resolve Gitea URLs.
giteaURL = envOrDefault("GITEA_URL", "http://localhost:3000")
giteaInURL = envOrDefault("GITEA_INTERNAL_URL", "http://gitea.git-system.svc.cluster.local:3000")
// Build clients from KUBECONFIG.
kubeconfig := os.Getenv("KUBECONFIG")
if kubeconfig == "" {
kubeconfig = clientcmd.RecommendedHomeFile
}
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
fmt.Fprintf(os.Stderr, "building kubeconfig: %v\n", err)
os.Exit(1)
}
kubeClient, err = kubernetes.NewForConfig(config)
if err != nil {
fmt.Fprintf(os.Stderr, "creating kubernetes client: %v\n", err)
os.Exit(1)
}
dynClient, err := dynamic.NewForConfig(config)
if err != nil {
fmt.Fprintf(os.Stderr, "creating dynamic client: %v\n", err)
os.Exit(1)
}
gitClient = gitclient.NewFromDynamic(dynClient)
os.Exit(m.Run())
}
func envOrDefault(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
// createGiteaRepo creates a repository on Gitea via the API.
func createGiteaRepo(t *testing.T, name string) {
t.Helper()
body, _ := json.Marshal(map[string]interface{}{
"name": name,
"auto_init": true,
"default_branch": "main",
})
req, err := http.NewRequest("POST", giteaURL+"/api/v1/user/repos", 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 repo %q: %v", name, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
t.Fatalf("creating Gitea repo %q: status %d", name, resp.StatusCode)
}
t.Logf("Created Gitea repo %q", name)
}
// getGiteaBranchCommit fetches the HEAD commit of a branch from Gitea.
func getGiteaBranchCommit(t *testing.T, repo, branch string) string {
t.Helper()
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/branches/%s", giteaURL, giteaUsername, repo, branch)
req, _ := http.NewRequest("GET", url, nil)
req.SetBasicAuth(giteaUsername, giteaPassword)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("getting branch %s/%s: %v", repo, branch, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("getting branch %s/%s: status %d", repo, branch, resp.StatusCode)
}
var result struct {
Commit struct {
ID string `json:"id"`
} `json:"commit"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("decoding branch response: %v", err)
}
return result.Commit.ID
}
// giteaBranchExists checks if a branch exists on Gitea.
func giteaBranchExists(t *testing.T, repo, branch string) bool {
t.Helper()
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/branches/%s", giteaURL, giteaUsername, repo, branch)
req, _ := http.NewRequest("GET", url, nil)
req.SetBasicAuth(giteaUsername, giteaPassword)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("checking branch %s/%s: %v", repo, branch, err)
}
resp.Body.Close()
return resp.StatusCode == http.StatusOK
}
// createCredentialsSecret creates a Secret with Gitea credentials.
// It returns the secret name. The secret is cleaned up when the test finishes.
func createCredentialsSecret(t *testing.T, name string) string {
t.Helper()
ctx := context.Background()
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: testNamespace,
},
StringData: map[string]string{
"username": giteaUsername,
"password": giteaPassword,
},
}
_, err := kubeClient.CoreV1().Secrets(testNamespace).Create(ctx, secret, metav1.CreateOptions{})
if err != nil {
t.Fatalf("creating secret %q: %v", name, err)
}
t.Cleanup(func() {
kubeClient.CoreV1().Secrets(testNamespace).Delete(ctx, name, metav1.DeleteOptions{})
})
return name
}
// createGitRepository creates a GitRepository CR pointing at a Gitea repo.
func createGitRepository(t *testing.T, name, repoName, secretName string) *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},
},
},
}
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
}
// createGitBranch creates a GitBranch CR.
func createGitBranch(t *testing.T, name, repoRef, branchName string) *gitv1alpha1.GitBranch {
t.Helper()
ctx := context.Background()
branch := &gitv1alpha1.GitBranch{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: testNamespace,
},
Spec: gitv1alpha1.GitBranchSpec{
RepositoryRef: repoRef,
BranchName: branchName,
},
}
created, err := gitClient.GitBranches(testNamespace).Create(ctx, branch, metav1.CreateOptions{})
if err != nil {
t.Fatalf("creating GitBranch %q: %v", name, err)
}
t.Cleanup(func() {
gitClient.GitBranches(testNamespace).Delete(ctx, name, metav1.DeleteOptions{})
})
return created
}
// waitForPushTransaction polls until the GitPushTransaction reaches a terminal phase.
func waitForPushTransaction(t *testing.T, name string) *gitv1alpha1.GitPushTransaction {
t.Helper()
ctx := context.Background()
deadline := time.Now().Add(pollTimeout)
for time.Now().Before(deadline) {
txn, err := gitClient.GitPushTransactions(testNamespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
t.Logf("getting push transaction %q: %v (retrying)", name, err)
time.Sleep(pollInterval)
continue
}
t.Logf("GitPushTransaction %q phase: %s", name, txn.Status.Phase)
switch txn.Status.Phase {
case gitv1alpha1.TransactionPhaseSucceeded:
return txn
case gitv1alpha1.TransactionPhaseFailed:
t.Fatalf("GitPushTransaction %q failed: %s", name, txn.Status.Message)
}
time.Sleep(pollInterval)
}
t.Fatalf("timed out waiting for GitPushTransaction %q", name)
return nil
}
// waitForGitBranchCommit polls until the GitBranch has a non-empty headCommit.
func waitForGitBranchCommit(t *testing.T, name string) 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 != "" {
return branch.Status.HeadCommit
}
time.Sleep(pollInterval)
}
t.Fatalf("timed out waiting for GitBranch %q headCommit", name)
return ""
}
// waitForSyncPhase polls until the GitRepoSync reaches a non-empty phase.
func waitForSyncPhase(t *testing.T, name string) *gitv1alpha1.GitRepoSync {
t.Helper()
ctx := context.Background()
deadline := time.Now().Add(pollTimeout)
for time.Now().Before(deadline) {
syncObj, err := gitClient.GitRepoSyncs(testNamespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
time.Sleep(pollInterval)
continue
}
if syncObj.Status.Phase != "" {
t.Logf("GitRepoSync %q phase: %s", name, syncObj.Status.Phase)
return syncObj
}
time.Sleep(pollInterval)
}
t.Fatalf("timed out waiting for GitRepoSync %q phase", name)
return nil
}

118
test/e2e/push_test.go Normal file
View file

@ -0,0 +1,118 @@
//go:build e2e
package e2e
import (
"context"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gitv1alpha1 "github.com/imjasonh/git-k8s/pkg/apis/git/v1alpha1"
)
func TestPushTransaction(t *testing.T) {
t.Parallel()
repoName := "e2e-push"
createGiteaRepo(t, repoName)
secretName := createCredentialsSecret(t, "e2e-push-creds")
createGitRepository(t, "e2e-push-repo", repoName, secretName)
createGitBranch(t, "e2e-push-feature", "e2e-push-repo", "feature-push")
// Create a push transaction: push main to a new branch.
ctx := context.Background()
txn := &gitv1alpha1.GitPushTransaction{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-push-txn",
Namespace: testNamespace,
},
Spec: gitv1alpha1.GitPushTransactionSpec{
RepositoryRef: "e2e-push-repo",
RefSpecs: []gitv1alpha1.PushRefSpec{
{
Source: "refs/heads/main",
Destination: "refs/heads/feature-push",
},
},
},
}
_, err := gitClient.GitPushTransactions(testNamespace).Create(ctx, txn, metav1.CreateOptions{})
if err != nil {
t.Fatalf("creating GitPushTransaction: %v", err)
}
t.Cleanup(func() {
gitClient.GitPushTransactions(testNamespace).Delete(ctx, "e2e-push-txn", metav1.DeleteOptions{})
})
// Wait for the push to succeed.
result := waitForPushTransaction(t, "e2e-push-txn")
if result.Status.ResultCommit == "" {
t.Fatal("push transaction succeeded but resultCommit is empty")
}
t.Logf("Push transaction result commit: %s", result.Status.ResultCommit)
// Verify the GitBranch was updated by the controller.
headCommit := waitForGitBranchCommit(t, "e2e-push-feature")
t.Logf("GitBranch headCommit: %s", headCommit)
// Verify the branch actually exists on the git server.
if !giteaBranchExists(t, repoName, "feature-push") {
t.Fatal("branch feature-push does not exist on Gitea after push")
}
}
func TestPushTransactionMultipleRefSpecs(t *testing.T) {
t.Parallel()
repoName := "e2e-push-multi"
createGiteaRepo(t, repoName)
secretName := createCredentialsSecret(t, "e2e-push-multi-creds")
createGitRepository(t, "e2e-push-multi-repo", repoName, secretName)
createGitBranch(t, "e2e-push-multi-b1", "e2e-push-multi-repo", "branch-one")
createGitBranch(t, "e2e-push-multi-b2", "e2e-push-multi-repo", "branch-two")
// Push main to two branches at once.
ctx := context.Background()
txn := &gitv1alpha1.GitPushTransaction{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-push-multi-txn",
Namespace: testNamespace,
},
Spec: gitv1alpha1.GitPushTransactionSpec{
RepositoryRef: "e2e-push-multi-repo",
RefSpecs: []gitv1alpha1.PushRefSpec{
{
Source: "refs/heads/main",
Destination: "refs/heads/branch-one",
},
{
Source: "refs/heads/main",
Destination: "refs/heads/branch-two",
},
},
},
}
_, err := gitClient.GitPushTransactions(testNamespace).Create(ctx, txn, metav1.CreateOptions{})
if err != nil {
t.Fatalf("creating GitPushTransaction: %v", err)
}
t.Cleanup(func() {
gitClient.GitPushTransactions(testNamespace).Delete(ctx, "e2e-push-multi-txn", metav1.DeleteOptions{})
})
result := waitForPushTransaction(t, "e2e-push-multi-txn")
if result.Status.ResultCommit == "" {
t.Fatal("push transaction succeeded but resultCommit is empty")
}
// Both branches should exist on Gitea.
if !giteaBranchExists(t, repoName, "branch-one") {
t.Error("branch-one does not exist on Gitea")
}
if !giteaBranchExists(t, repoName, "branch-two") {
t.Error("branch-two does not exist on Gitea")
}
}

77
test/e2e/sync_test.go Normal file
View file

@ -0,0 +1,77 @@
//go:build e2e
package e2e
import (
"context"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gitv1alpha1 "github.com/imjasonh/git-k8s/pkg/apis/git/v1alpha1"
)
func TestRepoSync(t *testing.T) {
t.Parallel()
// Create two repos on Gitea.
createGiteaRepo(t, "e2e-sync-a")
createGiteaRepo(t, "e2e-sync-b")
secretName := createCredentialsSecret(t, "e2e-sync-creds")
createGitRepository(t, "e2e-sync-repo-a", "e2e-sync-a", secretName)
createGitRepository(t, "e2e-sync-repo-b", "e2e-sync-b", secretName)
// Create GitBranch CRs and populate their headCommit via the push controller.
// First, push main to itself to get the push controller to set headCommit.
branchA := createGitBranch(t, "e2e-sync-branch-a", "e2e-sync-repo-a", "main")
branchB := createGitBranch(t, "e2e-sync-branch-b", "e2e-sync-repo-b", "main")
// We need headCommit set on both branches for the sync controller.
// Patch them with the actual commit from Gitea.
commitA := getGiteaBranchCommit(t, "e2e-sync-a", "main")
commitB := getGiteaBranchCommit(t, "e2e-sync-b", "main")
t.Logf("Repo A main commit: %s", commitA)
t.Logf("Repo B main commit: %s", commitB)
ctx := context.Background()
branchA.Status.HeadCommit = commitA
if _, err := gitClient.GitBranches(testNamespace).UpdateStatus(ctx, branchA, metav1.UpdateOptions{}); err != nil {
t.Fatalf("updating branch A status: %v", err)
}
branchB.Status.HeadCommit = commitB
if _, err := gitClient.GitBranches(testNamespace).UpdateStatus(ctx, branchB, metav1.UpdateOptions{}); err != nil {
t.Fatalf("updating branch B status: %v", err)
}
// Create a GitRepoSync between the two repos.
syncObj := &gitv1alpha1.GitRepoSync{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-sync",
Namespace: testNamespace,
},
Spec: gitv1alpha1.GitRepoSyncSpec{
RepoA: gitv1alpha1.SyncRepoRef{Name: "e2e-sync-repo-a"},
RepoB: gitv1alpha1.SyncRepoRef{Name: "e2e-sync-repo-b"},
BranchName: "main",
},
}
_, err := gitClient.GitRepoSyncs(testNamespace).Create(ctx, syncObj, metav1.CreateOptions{})
if err != nil {
t.Fatalf("creating GitRepoSync: %v", err)
}
t.Cleanup(func() {
gitClient.GitRepoSyncs(testNamespace).Delete(ctx, "e2e-sync", metav1.DeleteOptions{})
})
// Wait for the sync controller to set a phase.
result := waitForSyncPhase(t, "e2e-sync")
t.Logf("GitRepoSync phase: %s, message: %s", result.Status.Phase, result.Status.Message)
// The sync controller should have processed this.
// Since both repos are different auto_init repos, commits differ,
// so we expect Conflicted (no common ancestor) or another non-empty phase.
if result.Status.Phase == "" {
t.Fatal("sync controller did not set a phase")
}
}