mirror of
https://github.com/imjasonh/git-k8s
synced 2026-07-17 06:23:00 +00:00
Add unit tests and GitRepoSync e2e test
Add unit tests for all packages that previously had zero test coverage: - pkg/apis/git/v1alpha1: defaults, deepcopy, scheme registration, constants - pkg/client: toUnstructured/fromUnstructured round-trips, context injection - pkg/reconciler/push: splitKey, nestedFieldNoCopy, unstructuredNestedStringMap, base64 decoding - pkg/reconciler/sync: splitKey - pkg/reconciler/resolver: splitKey, changeName Add e2e test exercising the sync controller: - Creates second Gitea repo and GitRepository CR - Creates GitBranch CRs with headCommit set via status subresource patch - Creates GitRepoSync CR and verifies the sync controller processes it https://claude.ai/code/session_01QfFzqKQUsxxBsiZUhuJ3pG
This commit is contained in:
parent
3bbfe8b4d4
commit
4e4f5f923f
7 changed files with 1116 additions and 1 deletions
253
pkg/reconciler/push/push_test.go
Normal file
253
pkg/reconciler/push/push_test.go
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
package push
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
key string
|
||||
wantNS string
|
||||
wantName string
|
||||
}{
|
||||
{
|
||||
key: "default/my-resource",
|
||||
wantNS: "default",
|
||||
wantName: "my-resource",
|
||||
},
|
||||
{
|
||||
key: "kube-system/controller",
|
||||
wantNS: "kube-system",
|
||||
wantName: "controller",
|
||||
},
|
||||
{
|
||||
key: "no-namespace",
|
||||
wantNS: "",
|
||||
wantName: "no-namespace",
|
||||
},
|
||||
{
|
||||
key: "ns/name/with/slashes",
|
||||
wantNS: "ns",
|
||||
wantName: "name/with/slashes",
|
||||
},
|
||||
{
|
||||
key: "",
|
||||
wantNS: "",
|
||||
wantName: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.key, func(t *testing.T) {
|
||||
ns, name, err := splitKey(tt.key)
|
||||
if err != nil {
|
||||
t.Fatalf("splitKey(%q) error = %v", tt.key, err)
|
||||
}
|
||||
if ns != tt.wantNS {
|
||||
t.Errorf("namespace = %q, want %q", ns, tt.wantNS)
|
||||
}
|
||||
if name != tt.wantName {
|
||||
t.Errorf("name = %q, want %q", name, tt.wantName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedFieldNoCopy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj map[string]interface{}
|
||||
fields []string
|
||||
want interface{}
|
||||
found bool
|
||||
}{
|
||||
{
|
||||
name: "simple field",
|
||||
obj: map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
fields: []string{"key"},
|
||||
want: "value",
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
name: "nested field",
|
||||
obj: map[string]interface{}{
|
||||
"level1": map[string]interface{}{
|
||||
"level2": "deep-value",
|
||||
},
|
||||
},
|
||||
fields: []string{"level1", "level2"},
|
||||
want: "deep-value",
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
name: "missing field",
|
||||
obj: map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
fields: []string{"missing"},
|
||||
want: nil,
|
||||
found: false,
|
||||
},
|
||||
{
|
||||
name: "non-map intermediate",
|
||||
obj: map[string]interface{}{
|
||||
"key": "not-a-map",
|
||||
},
|
||||
fields: []string{"key", "subkey"},
|
||||
want: nil,
|
||||
found: false,
|
||||
},
|
||||
{
|
||||
name: "empty object",
|
||||
obj: map[string]interface{}{},
|
||||
fields: []string{"key"},
|
||||
want: nil,
|
||||
found: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, found, err := nestedFieldNoCopy(tt.obj, tt.fields...)
|
||||
if err != nil {
|
||||
t.Fatalf("nestedFieldNoCopy() error = %v", err)
|
||||
}
|
||||
if found != tt.found {
|
||||
t.Errorf("found = %v, want %v", found, tt.found)
|
||||
}
|
||||
if found && got != tt.want {
|
||||
t.Errorf("value = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnstructuredNestedStringMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj map[string]interface{}
|
||||
fields []string
|
||||
wantMap map[string]string
|
||||
wantFound bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid string map",
|
||||
obj: map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"username": base64.StdEncoding.EncodeToString([]byte("admin")),
|
||||
"password": base64.StdEncoding.EncodeToString([]byte("secret")),
|
||||
},
|
||||
},
|
||||
fields: []string{"data"},
|
||||
wantMap: map[string]string{
|
||||
"username": base64.StdEncoding.EncodeToString([]byte("admin")),
|
||||
"password": base64.StdEncoding.EncodeToString([]byte("secret")),
|
||||
},
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
name: "missing data field",
|
||||
obj: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{},
|
||||
},
|
||||
fields: []string{"data"},
|
||||
wantFound: false,
|
||||
},
|
||||
{
|
||||
name: "non-string values are skipped",
|
||||
obj: map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"string-key": "string-value",
|
||||
"int-key": 42,
|
||||
"bool-key": true,
|
||||
},
|
||||
},
|
||||
fields: []string{"data"},
|
||||
wantMap: map[string]string{
|
||||
"string-key": "string-value",
|
||||
},
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
name: "not a map type",
|
||||
obj: map[string]interface{}{
|
||||
"data": "just-a-string",
|
||||
},
|
||||
fields: []string{"data"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty map",
|
||||
obj: map[string]interface{}{
|
||||
"data": map[string]interface{}{},
|
||||
},
|
||||
fields: []string{"data"},
|
||||
wantMap: map[string]string{},
|
||||
wantFound: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, found, err := unstructuredNestedStringMap(tt.obj, tt.fields...)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if found != tt.wantFound {
|
||||
t.Errorf("found = %v, want %v", found, tt.wantFound)
|
||||
}
|
||||
if tt.wantMap != nil {
|
||||
if len(got) != len(tt.wantMap) {
|
||||
t.Errorf("map length = %d, want %d", len(got), len(tt.wantMap))
|
||||
}
|
||||
for k, v := range tt.wantMap {
|
||||
if got[k] != v {
|
||||
t.Errorf("map[%q] = %q, want %q", k, got[k], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBase64Decoding(t *testing.T) {
|
||||
// Verify that values typically stored in Kubernetes secrets
|
||||
// (base64-encoded via dynamic client) decode correctly.
|
||||
tests := []struct {
|
||||
name string
|
||||
encoded string
|
||||
wantText string
|
||||
}{
|
||||
{
|
||||
name: "simple username",
|
||||
encoded: base64.StdEncoding.EncodeToString([]byte("admin")),
|
||||
wantText: "admin",
|
||||
},
|
||||
{
|
||||
name: "password with special chars",
|
||||
encoded: base64.StdEncoding.EncodeToString([]byte("p@ss!w0rd#123")),
|
||||
wantText: "p@ss!w0rd#123",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
encoded: base64.StdEncoding.EncodeToString([]byte("")),
|
||||
wantText: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
decoded, err := base64.StdEncoding.DecodeString(tt.encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeString() error = %v", err)
|
||||
}
|
||||
if string(decoded) != tt.wantText {
|
||||
t.Errorf("decoded = %q, want %q", string(decoded), tt.wantText)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
101
pkg/reconciler/resolver/resolver_test.go
Normal file
101
pkg/reconciler/resolver/resolver_test.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
func TestSplitKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
key string
|
||||
wantNS string
|
||||
wantName string
|
||||
}{
|
||||
{
|
||||
key: "default/my-sync",
|
||||
wantNS: "default",
|
||||
wantName: "my-sync",
|
||||
},
|
||||
{
|
||||
key: "git-system/resolver",
|
||||
wantNS: "git-system",
|
||||
wantName: "resolver",
|
||||
},
|
||||
{
|
||||
key: "just-name",
|
||||
wantNS: "",
|
||||
wantName: "just-name",
|
||||
},
|
||||
{
|
||||
key: "",
|
||||
wantNS: "",
|
||||
wantName: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.key, func(t *testing.T) {
|
||||
ns, name, err := splitKey(tt.key)
|
||||
if err != nil {
|
||||
t.Fatalf("splitKey(%q) error = %v", tt.key, err)
|
||||
}
|
||||
if ns != tt.wantNS {
|
||||
t.Errorf("namespace = %q, want %q", ns, tt.wantNS)
|
||||
}
|
||||
if name != tt.wantName {
|
||||
t.Errorf("name = %q, want %q", name, tt.wantName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
change *object.Change
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "from name takes precedence",
|
||||
change: &object.Change{
|
||||
From: object.ChangeEntry{Name: "file-from.txt"},
|
||||
To: object.ChangeEntry{Name: "file-to.txt"},
|
||||
},
|
||||
want: "file-from.txt",
|
||||
},
|
||||
{
|
||||
name: "to name used when from is empty (new file)",
|
||||
change: &object.Change{
|
||||
From: object.ChangeEntry{Name: ""},
|
||||
To: object.ChangeEntry{Name: "new-file.txt"},
|
||||
},
|
||||
want: "new-file.txt",
|
||||
},
|
||||
{
|
||||
name: "rename uses from name",
|
||||
change: &object.Change{
|
||||
From: object.ChangeEntry{Name: "old-name.txt"},
|
||||
To: object.ChangeEntry{Name: "new-name.txt"},
|
||||
},
|
||||
want: "old-name.txt",
|
||||
},
|
||||
{
|
||||
name: "deleted file (no to name)",
|
||||
change: &object.Change{
|
||||
From: object.ChangeEntry{Name: "deleted.txt"},
|
||||
To: object.ChangeEntry{Name: ""},
|
||||
},
|
||||
want: "deleted.txt",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := changeName(tt.change)
|
||||
if got != tt.want {
|
||||
t.Errorf("changeName() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
54
pkg/reconciler/sync/sync_test.go
Normal file
54
pkg/reconciler/sync/sync_test.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package sync
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
key string
|
||||
wantNS string
|
||||
wantName string
|
||||
}{
|
||||
{
|
||||
key: "default/my-sync",
|
||||
wantNS: "default",
|
||||
wantName: "my-sync",
|
||||
},
|
||||
{
|
||||
key: "git-system/sync-controller",
|
||||
wantNS: "git-system",
|
||||
wantName: "sync-controller",
|
||||
},
|
||||
{
|
||||
key: "just-name",
|
||||
wantNS: "",
|
||||
wantName: "just-name",
|
||||
},
|
||||
{
|
||||
key: "",
|
||||
wantNS: "",
|
||||
wantName: "",
|
||||
},
|
||||
{
|
||||
key: "ns/name/extra",
|
||||
wantNS: "ns",
|
||||
wantName: "name/extra",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.key, func(t *testing.T) {
|
||||
ns, name, err := splitKey(tt.key)
|
||||
if err != nil {
|
||||
t.Fatalf("splitKey(%q) error = %v", tt.key, err)
|
||||
}
|
||||
if ns != tt.wantNS {
|
||||
t.Errorf("namespace = %q, want %q", ns, tt.wantNS)
|
||||
}
|
||||
if name != tt.wantName {
|
||||
t.Errorf("name = %q, want %q", name, tt.wantName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue