1
0
Fork 0
mirror of https://github.com/imjasonh/git-k8s synced 2026-07-13 08:57:00 +00:00
git-k8s/pkg/client/clientset_test.go
Claude 01fcebed54
Improve test coverage from 10.3% to 42.7% with diffcover analysis
Apply the diffcover technique to map code snippets to features, identify
under-tested components, and add comprehensive tests for all reconciler
business logic. Key improvements:

- pkg/reconciler/internal: 26.7% → 100% (generic adapter fully tested)
- pkg/client: 5.6% → 71.8% (full CRUD for all 4 resource types)
- pkg/reconciler/repowatcher: 10.1% → 56.3% (ReconcileKind lifecycle)
- pkg/reconciler/push: 0% → 33.3% (failTransaction, updateBranches)
- pkg/reconciler/sync: 0% → 33.0% (findBranch, createPushTransaction)
- pkg/reconciler/resolver: 2.1% → 17.0% (merge transactions, manual intervention)

Includes COVERAGE_REPORT.md with full code-to-feature mapping showing
which tests cover which lines, following the diffcover methodology.

https://claude.ai/code/session_016Yeyfoocn7SN2HuMsxRD6f
2026-03-04 19:49:59 +00:00

579 lines
16 KiB
Go

package client
import (
"context"
"encoding/json"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
fakedynamic "k8s.io/client-go/dynamic/fake"
gitv1alpha1 "github.com/imjasonh/git-k8s/pkg/apis/git/v1alpha1"
)
func newFakeScheme() *runtime.Scheme {
scheme := runtime.NewScheme()
for _, kind := range []string{
"GitRepository", "GitBranch", "GitPushTransaction", "GitRepoSync",
} {
scheme.AddKnownTypeWithName(
schema.GroupVersionKind{Group: "git-k8s.imjasonh.com", Version: "v1alpha1", Kind: kind},
&unstructured.Unstructured{},
)
scheme.AddKnownTypeWithName(
schema.GroupVersionKind{Group: "git-k8s.imjasonh.com", Version: "v1alpha1", Kind: kind + "List"},
&unstructured.UnstructuredList{},
)
}
return scheme
}
var customListKinds = map[schema.GroupVersionResource]string{
{Group: "git-k8s.imjasonh.com", Version: "v1alpha1", Resource: "gitrepositories"}: "GitRepositoryList",
{Group: "git-k8s.imjasonh.com", Version: "v1alpha1", Resource: "gitbranches"}: "GitBranchList",
{Group: "git-k8s.imjasonh.com", Version: "v1alpha1", Resource: "gitpushtransactions"}: "GitPushTransactionList",
{Group: "git-k8s.imjasonh.com", Version: "v1alpha1", Resource: "gitreposyncs"}: "GitRepoSyncList",
}
func newFakeClient(objects ...runtime.Object) *GitV1alpha1Client {
dynClient := fakedynamic.NewSimpleDynamicClientWithCustomListKinds(newFakeScheme(), customListKinds, objects...)
return NewFromDynamic(dynClient)
}
func TestToUnstructured(t *testing.T) {
repo := &gitv1alpha1.GitRepository{
TypeMeta: metav1.TypeMeta{
APIVersion: "git-k8s.imjasonh.com/v1alpha1",
Kind: "GitRepository",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
Namespace: "default",
},
Spec: gitv1alpha1.GitRepositorySpec{
URL: "https://github.com/example/repo.git",
DefaultBranch: "main",
},
}
u, err := toUnstructured(repo)
if err != nil {
t.Fatalf("toUnstructured() error = %v", err)
}
// Verify key fields in the unstructured representation.
if got := u.GetName(); got != "test-repo" {
t.Errorf("Name = %q, want %q", got, "test-repo")
}
if got := u.GetNamespace(); got != "default" {
t.Errorf("Namespace = %q, want %q", got, "default")
}
spec, ok := u.Object["spec"].(map[string]interface{})
if !ok {
t.Fatal("spec field not found or wrong type")
}
if got := spec["url"]; got != "https://github.com/example/repo.git" {
t.Errorf("spec.url = %v, want %q", got, "https://github.com/example/repo.git")
}
}
func TestFromUnstructured(t *testing.T) {
u := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "git-k8s.imjasonh.com/v1alpha1",
"kind": "GitRepository",
"metadata": map[string]interface{}{
"name": "test-repo",
"namespace": "default",
},
"spec": map[string]interface{}{
"url": "https://github.com/example/repo.git",
"defaultBranch": "main",
},
},
}
repo := &gitv1alpha1.GitRepository{}
if err := fromUnstructured(u, repo); err != nil {
t.Fatalf("fromUnstructured() error = %v", err)
}
if got := repo.Name; got != "test-repo" {
t.Errorf("Name = %q, want %q", got, "test-repo")
}
if got := repo.Spec.URL; got != "https://github.com/example/repo.git" {
t.Errorf("URL = %q, want %q", got, "https://github.com/example/repo.git")
}
if got := repo.Spec.DefaultBranch; got != "main" {
t.Errorf("DefaultBranch = %q, want %q", got, "main")
}
}
func TestRoundTrip_GitRepository(t *testing.T) {
orig := &gitv1alpha1.GitRepository{
TypeMeta: metav1.TypeMeta{
APIVersion: gitv1alpha1.SchemeGroupVersion.String(),
Kind: "GitRepository",
},
ObjectMeta: metav1.ObjectMeta{
Name: "round-trip",
Namespace: "test-ns",
},
Spec: gitv1alpha1.GitRepositorySpec{
URL: "https://github.com/test/repo.git",
DefaultBranch: "develop",
Auth: &gitv1alpha1.GitAuth{
SecretRef: &gitv1alpha1.SecretRef{Name: "creds"},
},
},
}
u, err := toUnstructured(orig)
if err != nil {
t.Fatalf("toUnstructured() error = %v", err)
}
result := &gitv1alpha1.GitRepository{}
if err := fromUnstructured(u, result); err != nil {
t.Fatalf("fromUnstructured() error = %v", err)
}
if result.Name != orig.Name {
t.Errorf("Name = %q, want %q", result.Name, orig.Name)
}
if result.Spec.URL != orig.Spec.URL {
t.Errorf("URL = %q, want %q", result.Spec.URL, orig.Spec.URL)
}
if result.Spec.DefaultBranch != orig.Spec.DefaultBranch {
t.Errorf("DefaultBranch = %q, want %q", result.Spec.DefaultBranch, orig.Spec.DefaultBranch)
}
if result.Spec.Auth == nil || result.Spec.Auth.SecretRef == nil {
t.Fatal("Auth.SecretRef is nil after round trip")
}
if result.Spec.Auth.SecretRef.Name != "creds" {
t.Errorf("SecretRef.Name = %q, want %q", result.Spec.Auth.SecretRef.Name, "creds")
}
}
func TestRoundTrip_GitPushTransaction(t *testing.T) {
orig := &gitv1alpha1.GitPushTransaction{
TypeMeta: metav1.TypeMeta{
APIVersion: gitv1alpha1.SchemeGroupVersion.String(),
Kind: "GitPushTransaction",
},
ObjectMeta: metav1.ObjectMeta{
Name: "push-1",
Namespace: "default",
},
Spec: gitv1alpha1.GitPushTransactionSpec{
RepositoryRef: "my-repo",
Atomic: true,
RefSpecs: []gitv1alpha1.PushRefSpec{
{
Source: "refs/heads/main",
Destination: "refs/heads/feature",
ExpectedOldCommit: "abc123",
},
{
Source: "refs/heads/develop",
Destination: "refs/heads/staging",
},
},
},
}
u, err := toUnstructured(orig)
if err != nil {
t.Fatalf("toUnstructured() error = %v", err)
}
result := &gitv1alpha1.GitPushTransaction{}
if err := fromUnstructured(u, result); err != nil {
t.Fatalf("fromUnstructured() error = %v", err)
}
if result.Spec.RepositoryRef != "my-repo" {
t.Errorf("RepositoryRef = %q, want %q", result.Spec.RepositoryRef, "my-repo")
}
if !result.Spec.Atomic {
t.Error("Atomic = false, want true")
}
if len(result.Spec.RefSpecs) != 2 {
t.Fatalf("RefSpecs length = %d, want 2", len(result.Spec.RefSpecs))
}
if result.Spec.RefSpecs[0].ExpectedOldCommit != "abc123" {
t.Errorf("RefSpecs[0].ExpectedOldCommit = %q, want %q", result.Spec.RefSpecs[0].ExpectedOldCommit, "abc123")
}
if result.Spec.RefSpecs[1].ExpectedOldCommit != "" {
t.Errorf("RefSpecs[1].ExpectedOldCommit = %q, want empty", result.Spec.RefSpecs[1].ExpectedOldCommit)
}
}
func TestRoundTrip_GitRepoSync(t *testing.T) {
orig := &gitv1alpha1.GitRepoSync{
TypeMeta: metav1.TypeMeta{
APIVersion: gitv1alpha1.SchemeGroupVersion.String(),
Kind: "GitRepoSync",
},
ObjectMeta: metav1.ObjectMeta{
Name: "sync-1",
Namespace: "default",
},
Spec: gitv1alpha1.GitRepoSyncSpec{
RepoA: gitv1alpha1.SyncRepoRef{Name: "repo-a"},
RepoB: gitv1alpha1.SyncRepoRef{Name: "repo-b"},
BranchName: "main",
},
}
u, err := toUnstructured(orig)
if err != nil {
t.Fatalf("toUnstructured() error = %v", err)
}
result := &gitv1alpha1.GitRepoSync{}
if err := fromUnstructured(u, result); err != nil {
t.Fatalf("fromUnstructured() error = %v", err)
}
if result.Spec.RepoA.Name != "repo-a" {
t.Errorf("RepoA.Name = %q, want %q", result.Spec.RepoA.Name, "repo-a")
}
if result.Spec.RepoB.Name != "repo-b" {
t.Errorf("RepoB.Name = %q, want %q", result.Spec.RepoB.Name, "repo-b")
}
if result.Spec.BranchName != "main" {
t.Errorf("BranchName = %q, want %q", result.Spec.BranchName, "main")
}
}
func TestContextInjection(t *testing.T) {
client := &GitV1alpha1Client{}
ctx := context.Background()
ctx = WithClient(ctx, client)
got := Get(ctx)
if got != client {
t.Error("Get(ctx) returned different client than WithClient stored")
}
}
func TestGVRConstants(t *testing.T) {
tests := []struct {
name string
gvr interface{ String() string }
resource string
}{
{"gitrepositories", gitRepositoryGVR, "gitrepositories"},
{"gitbranches", gitBranchGVR, "gitbranches"},
{"gitpushtransactions", gitPushTransactionGVR, "gitpushtransactions"},
{"gitreposyncs", gitRepoSyncGVR, "gitreposyncs"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.gvr.String()
want := gitv1alpha1.GroupName + "/" + gitv1alpha1.Version
if got != want+", Resource="+tt.resource {
t.Errorf("GVR = %q, want group/version = %q resource = %q", got, want, tt.resource)
}
})
}
}
func TestToUnstructured_PreservesJSON(t *testing.T) {
branch := &gitv1alpha1.GitBranch{
TypeMeta: metav1.TypeMeta{
APIVersion: gitv1alpha1.SchemeGroupVersion.String(),
Kind: "GitBranch",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-branch",
Namespace: "ns",
},
Spec: gitv1alpha1.GitBranchSpec{
RepositoryRef: "my-repo",
BranchName: "feature/awesome",
},
}
u, err := toUnstructured(branch)
if err != nil {
t.Fatalf("toUnstructured() error = %v", err)
}
// Verify the JSON is valid by marshalling back.
data, err := json.Marshal(u.Object)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var roundTrip map[string]interface{}
if err := json.Unmarshal(data, &roundTrip); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
spec, ok := roundTrip["spec"].(map[string]interface{})
if !ok {
t.Fatal("spec not found in roundTrip")
}
if got := spec["repositoryRef"]; got != "my-repo" {
t.Errorf("repositoryRef = %v, want %q", got, "my-repo")
}
if got := spec["branchName"]; got != "feature/awesome" {
t.Errorf("branchName = %v, want %q", got, "feature/awesome")
}
}
func TestNewFromDynamic(t *testing.T) {
dynClient := fakedynamic.NewSimpleDynamicClient(newFakeScheme())
gc := NewFromDynamic(dynClient)
if gc == nil {
t.Fatal("NewFromDynamic returned nil")
}
if gc.client != dynClient {
t.Error("NewFromDynamic did not store the dynamic client")
}
}
func TestGitRepositories_CRUD(t *testing.T) {
c := newFakeClient()
ctx := context.Background()
// Create.
repo := &gitv1alpha1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
Namespace: "default",
},
Spec: gitv1alpha1.GitRepositorySpec{
URL: "https://example.com/repo.git",
DefaultBranch: "main",
},
}
created, err := c.GitRepositories("default").Create(ctx, repo, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Create() error = %v", err)
}
if created.Name != "test-repo" {
t.Errorf("Name = %q, want %q", created.Name, "test-repo")
}
// Get.
got, err := c.GitRepositories("default").Get(ctx, "test-repo", metav1.GetOptions{})
if err != nil {
t.Fatalf("Get() error = %v", err)
}
if got.Spec.URL != "https://example.com/repo.git" {
t.Errorf("URL = %q, want %q", got.Spec.URL, "https://example.com/repo.git")
}
// Update.
got.Spec.DefaultBranch = "develop"
updated, err := c.GitRepositories("default").Update(ctx, got, metav1.UpdateOptions{})
if err != nil {
t.Fatalf("Update() error = %v", err)
}
if updated.Spec.DefaultBranch != "develop" {
t.Errorf("DefaultBranch = %q, want %q", updated.Spec.DefaultBranch, "develop")
}
// UpdateStatus.
now := metav1.Now()
updated.Status.LastFetchTime = &now
statusUpdated, err := c.GitRepositories("default").UpdateStatus(ctx, updated, metav1.UpdateOptions{})
if err != nil {
t.Fatalf("UpdateStatus() error = %v", err)
}
if statusUpdated.Status.LastFetchTime == nil {
t.Error("LastFetchTime should be set")
}
// List.
list, err := c.GitRepositories("default").List(ctx, metav1.ListOptions{})
if err != nil {
t.Fatalf("List() error = %v", err)
}
if len(list.Items) != 1 {
t.Errorf("List Items = %d, want 1", len(list.Items))
}
// Delete.
if err := c.GitRepositories("default").Delete(ctx, "test-repo", metav1.DeleteOptions{}); err != nil {
t.Fatalf("Delete() error = %v", err)
}
list, err = c.GitRepositories("default").List(ctx, metav1.ListOptions{})
if err != nil {
t.Fatalf("List() error = %v", err)
}
if len(list.Items) != 0 {
t.Errorf("List after Delete = %d, want 0", len(list.Items))
}
}
func TestGitBranches_CRUD(t *testing.T) {
c := newFakeClient()
ctx := context.Background()
branch := &gitv1alpha1.GitBranch{
ObjectMeta: metav1.ObjectMeta{
Name: "test-branch",
Namespace: "default",
},
Spec: gitv1alpha1.GitBranchSpec{
RepositoryRef: "my-repo",
BranchName: "main",
},
}
created, err := c.GitBranches("default").Create(ctx, branch, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Create() error = %v", err)
}
if created.Spec.BranchName != "main" {
t.Errorf("BranchName = %q, want %q", created.Spec.BranchName, "main")
}
got, err := c.GitBranches("default").Get(ctx, "test-branch", metav1.GetOptions{})
if err != nil {
t.Fatalf("Get() error = %v", err)
}
got.Status.HeadCommit = "abc123"
_, err = c.GitBranches("default").UpdateStatus(ctx, got, metav1.UpdateOptions{})
if err != nil {
t.Fatalf("UpdateStatus() error = %v", err)
}
got.Spec.BranchName = "develop"
_, err = c.GitBranches("default").Update(ctx, got, metav1.UpdateOptions{})
if err != nil {
t.Fatalf("Update() error = %v", err)
}
list, err := c.GitBranches("default").List(ctx, metav1.ListOptions{})
if err != nil {
t.Fatalf("List() error = %v", err)
}
if len(list.Items) != 1 {
t.Errorf("List Items = %d, want 1", len(list.Items))
}
if err := c.GitBranches("default").Delete(ctx, "test-branch", metav1.DeleteOptions{}); err != nil {
t.Fatalf("Delete() error = %v", err)
}
}
func TestGitPushTransactions_CRUD(t *testing.T) {
c := newFakeClient()
ctx := context.Background()
txn := &gitv1alpha1.GitPushTransaction{
ObjectMeta: metav1.ObjectMeta{
Name: "push-1",
Namespace: "default",
},
Spec: gitv1alpha1.GitPushTransactionSpec{
RepositoryRef: "my-repo",
Atomic: true,
RefSpecs: []gitv1alpha1.PushRefSpec{
{Source: "refs/heads/main", Destination: "refs/heads/main"},
},
},
}
created, err := c.GitPushTransactions("default").Create(ctx, txn, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Create() error = %v", err)
}
if !created.Spec.Atomic {
t.Error("Atomic should be true")
}
got, err := c.GitPushTransactions("default").Get(ctx, "push-1", metav1.GetOptions{})
if err != nil {
t.Fatalf("Get() error = %v", err)
}
got.Status.Phase = gitv1alpha1.TransactionPhaseSucceeded
_, err = c.GitPushTransactions("default").UpdateStatus(ctx, got, metav1.UpdateOptions{})
if err != nil {
t.Fatalf("UpdateStatus() error = %v", err)
}
got.Spec.Atomic = false
_, err = c.GitPushTransactions("default").Update(ctx, got, metav1.UpdateOptions{})
if err != nil {
t.Fatalf("Update() error = %v", err)
}
list, err := c.GitPushTransactions("default").List(ctx, metav1.ListOptions{})
if err != nil {
t.Fatalf("List() error = %v", err)
}
if len(list.Items) != 1 {
t.Errorf("List Items = %d, want 1", len(list.Items))
}
if err := c.GitPushTransactions("default").Delete(ctx, "push-1", metav1.DeleteOptions{}); err != nil {
t.Fatalf("Delete() error = %v", err)
}
}
func TestGitRepoSyncs_CRUD(t *testing.T) {
c := newFakeClient()
ctx := context.Background()
sync := &gitv1alpha1.GitRepoSync{
ObjectMeta: metav1.ObjectMeta{
Name: "sync-1",
Namespace: "default",
},
Spec: gitv1alpha1.GitRepoSyncSpec{
RepoA: gitv1alpha1.SyncRepoRef{Name: "repo-a"},
RepoB: gitv1alpha1.SyncRepoRef{Name: "repo-b"},
BranchName: "main",
},
}
created, err := c.GitRepoSyncs("default").Create(ctx, sync, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Create() error = %v", err)
}
if created.Spec.BranchName != "main" {
t.Errorf("BranchName = %q, want %q", created.Spec.BranchName, "main")
}
got, err := c.GitRepoSyncs("default").Get(ctx, "sync-1", metav1.GetOptions{})
if err != nil {
t.Fatalf("Get() error = %v", err)
}
got.Status.Phase = gitv1alpha1.SyncPhaseInSync
_, err = c.GitRepoSyncs("default").UpdateStatus(ctx, got, metav1.UpdateOptions{})
if err != nil {
t.Fatalf("UpdateStatus() error = %v", err)
}
got.Spec.BranchName = "develop"
_, err = c.GitRepoSyncs("default").Update(ctx, got, metav1.UpdateOptions{})
if err != nil {
t.Fatalf("Update() error = %v", err)
}
list, err := c.GitRepoSyncs("default").List(ctx, metav1.ListOptions{})
if err != nil {
t.Fatalf("List() error = %v", err)
}
if len(list.Items) != 1 {
t.Errorf("List Items = %d, want 1", len(list.Items))
}
if err := c.GitRepoSyncs("default").Delete(ctx, "sync-1", metav1.DeleteOptions{}); err != nil {
t.Fatalf("Delete() error = %v", err)
}
}