package k8s import ( "os" "path/filepath" "runtime" "strings" "testing" "k8s.io/apimachinery/pkg/util/yaml" ) func toInt64(v interface{}) int64 { switch n := v.(type) { case int64: return n case float64: return int64(n) case int: return int64(n) default: return 0 } } func forgejoYAMLPath() string { _, thisFile, _, _ := runtime.Caller(0) return filepath.Join(filepath.Dir(thisFile), "..", "..", "..", "..", "k8s", "dev-infra", "forgejo.yaml") } func readForgejoDocuments(t *testing.T) []map[string]interface{} { t.Helper() data, err := os.ReadFile(forgejoYAMLPath()) if err != nil { t.Fatalf("reading forgejo.yaml: %v", err) } var docs []map[string]interface{} parts := strings.Split(string(data), "\n---") for _, part := range parts { trimmed := strings.TrimSpace(part) if trimmed == "" { continue } // Strip leading comment lines (file-level comments before first doc) lines := strings.Split(trimmed, "\n") var contentLines []string for _, line := range lines { stripped := strings.TrimSpace(line) if len(contentLines) == 0 && (stripped == "" || strings.HasPrefix(stripped, "#")) { continue } contentLines = append(contentLines, line) } content := strings.Join(contentLines, "\n") if content == "" { continue } var doc map[string]interface{} if err := yaml.NewYAMLOrJSONDecoder(strings.NewReader(content), 4096).Decode(&doc); err != nil { t.Fatalf("decoding YAML document: %v\n---\n%s", err, content[:min(200, len(content))]) } docs = append(docs, doc) } return docs } func findDoc(docs []map[string]interface{}, kind, name string) map[string]interface{} { for _, doc := range docs { if doc["kind"] == kind { meta, ok := doc["metadata"].(map[string]interface{}) if ok && meta["name"] == name { return doc } } } return nil } func TestForgejoYAMLExists(t *testing.T) { path := forgejoYAMLPath() if _, err := os.Stat(path); os.IsNotExist(err) { t.Fatalf("forgejo.yaml does not exist at %s", path) } } func TestForgejoYAMLDocumentCount(t *testing.T) { docs := readForgejoDocuments(t) // Expected: Secret (admin), ConfigMap, PVC, Deployment, Service, IngressRoute, Middleware (basic-auth), Secret (htpasswd), Middleware (trailing-slash) if len(docs) < 9 { kinds := make([]string, len(docs)) for i, d := range docs { kinds[i] = d["kind"].(string) } t.Errorf("expected at least 9 YAML documents, got %d: %v", len(docs), kinds) } } func TestForgejoAdminSecret(t *testing.T) { docs := readForgejoDocuments(t) doc := findDoc(docs, "Secret", "forgejo-admin-secret") if doc == nil { t.Fatal("missing forgejo-admin-secret Secret") } meta := doc["metadata"].(map[string]interface{}) if meta["namespace"] != "dev-infra" { t.Errorf("expected namespace dev-infra, got %v", meta["namespace"]) } } func TestForgejoConfigMap(t *testing.T) { docs := readForgejoDocuments(t) doc := findDoc(docs, "ConfigMap", "forgejo-config") if doc == nil { t.Fatal("missing forgejo-config ConfigMap") } meta := doc["metadata"].(map[string]interface{}) if meta["namespace"] != "dev-infra" { t.Errorf("expected namespace dev-infra, got %v", meta["namespace"]) } data := doc["data"].(map[string]interface{}) appIni, ok := data["app.ini"].(string) if !ok { t.Fatal("missing app.ini in ConfigMap data") } requiredSettings := []string{ "ROOT_URL = https://spinoff.dev/forgejo/", "HTTP_PORT = 3000", "DB_TYPE = sqlite3", "ENABLED = true", "DISABLE_REGISTRATION = true", "OFFLINE_MODE = true", "DEFAULT_BRANCH = main", } for _, s := range requiredSettings { if !strings.Contains(appIni, s) { t.Errorf("app.ini missing required setting: %s", s) } } } func TestForgejoPVC(t *testing.T) { docs := readForgejoDocuments(t) doc := findDoc(docs, "PersistentVolumeClaim", "forgejo-storage") if doc == nil { t.Fatal("missing forgejo-storage PVC") } meta := doc["metadata"].(map[string]interface{}) if meta["namespace"] != "dev-infra" { t.Errorf("expected namespace dev-infra, got %v", meta["namespace"]) } spec := doc["spec"].(map[string]interface{}) storageClass, ok := spec["storageClassName"].(string) if !ok || storageClass != "longhorn" { t.Errorf("expected storageClassName longhorn, got %v", storageClass) } resources := spec["resources"].(map[string]interface{}) requests := resources["requests"].(map[string]interface{}) if requests["storage"] != "5Gi" { t.Errorf("expected 5Gi storage, got %v", requests["storage"]) } } func TestForgejoDeployment(t *testing.T) { docs := readForgejoDocuments(t) doc := findDoc(docs, "Deployment", "forgejo") if doc == nil { t.Fatal("missing forgejo Deployment") } meta := doc["metadata"].(map[string]interface{}) if meta["namespace"] != "dev-infra" { t.Errorf("expected namespace dev-infra, got %v", meta["namespace"]) } labels := meta["labels"].(map[string]interface{}) if labels["app"] != "forgejo" { t.Errorf("expected label app=forgejo, got %v", labels["app"]) } spec := doc["spec"].(map[string]interface{}) strategy := spec["strategy"].(map[string]interface{}) if strategy["type"] != "Recreate" { t.Errorf("expected Recreate strategy, got %v", strategy["type"]) } template := spec["template"].(map[string]interface{}) podSpec := template["spec"].(map[string]interface{}) t.Run("init_containers", func(t *testing.T) { initContainers, ok := podSpec["initContainers"].([]interface{}) if !ok || len(initContainers) < 2 { t.Fatal("expected at least 2 init containers (setup-config and create-admin)") } setupC := initContainers[0].(map[string]interface{}) if setupC["name"] != "setup-config" { t.Errorf("expected first init container name setup-config, got %v", setupC["name"]) } initC := initContainers[1].(map[string]interface{}) if initC["name"] != "create-admin" { t.Errorf("expected second init container name create-admin, got %v", initC["name"]) } image, _ := initC["image"].(string) if !strings.HasPrefix(image, "codeberg.org/forgejo/forgejo:") { t.Errorf("expected forgejo image, got %v", image) } }) t.Run("containers", func(t *testing.T) { containers := podSpec["containers"].([]interface{}) if len(containers) != 2 { t.Fatalf("expected 2 containers (forgejo + ipip-sidecar), got %d", len(containers)) } forgejo := containers[0].(map[string]interface{}) if forgejo["name"] != "forgejo" { t.Errorf("expected container name forgejo, got %v", forgejo["name"]) } image, _ := forgejo["image"].(string) if !strings.HasPrefix(image, "codeberg.org/forgejo/forgejo:") { t.Errorf("expected forgejo image, got %v", image) } ports := forgejo["ports"].([]interface{}) if len(ports) != 1 { t.Errorf("expected 1 port, got %d", len(ports)) } port := ports[0].(map[string]interface{}) containerPort := toInt64(port["containerPort"]) if containerPort != 3000 { t.Errorf("expected port 3000, got %v", port["containerPort"]) } ipip := containers[1].(map[string]interface{}) if ipip["name"] != "ipip-sidecar" { t.Errorf("expected container name ipip-sidecar, got %v", ipip["name"]) } }) t.Run("volumes", func(t *testing.T) { volumes := podSpec["volumes"].([]interface{}) if len(volumes) < 2 { t.Fatalf("expected at least 2 volumes (storage + config), got %d", len(volumes)) } volumeNames := make(map[string]bool) for _, v := range volumes { vol := v.(map[string]interface{}) volumeNames[vol["name"].(string)] = true } if !volumeNames["storage"] { t.Error("missing storage volume") } if !volumeNames["config"] { t.Error("missing config volume") } }) } func TestForgejoService(t *testing.T) { docs := readForgejoDocuments(t) doc := findDoc(docs, "Service", "forgejo") if doc == nil { t.Fatal("missing forgejo Service") } meta := doc["metadata"].(map[string]interface{}) if meta["namespace"] != "dev-infra" { t.Errorf("expected namespace dev-infra, got %v", meta["namespace"]) } spec := doc["spec"].(map[string]interface{}) if spec["type"] != "ClusterIP" { t.Errorf("expected ClusterIP, got %v", spec["type"]) } ports := spec["ports"].([]interface{}) if len(ports) != 1 { t.Fatalf("expected 1 port, got %d", len(ports)) } port := ports[0].(map[string]interface{}) portNum := toInt64(port["port"]) if portNum != 3000 { t.Errorf("expected port 3000, got %v", port["port"]) } selector := spec["selector"].(map[string]interface{}) if selector["app"] != "forgejo" { t.Errorf("expected selector app=forgejo, got %v", selector["app"]) } } func TestForgejoIngressRoute(t *testing.T) { docs := readForgejoDocuments(t) doc := findDoc(docs, "IngressRoute", "forgejo-ingress") if doc == nil { t.Fatal("missing forgejo-ingress IngressRoute") } meta := doc["metadata"].(map[string]interface{}) if meta["namespace"] != "dev-infra" { t.Errorf("expected namespace dev-infra, got %v", meta["namespace"]) } spec := doc["spec"].(map[string]interface{}) entryPoints := spec["entryPoints"].([]interface{}) if len(entryPoints) != 2 { t.Errorf("expected 2 entryPoints (web, websecure), got %d", len(entryPoints)) } routes := spec["routes"].([]interface{}) if len(routes) < 2 { t.Fatalf("expected at least 2 routes (main + trailing-slash redirect), got %d", len(routes)) } mainRoute := routes[0].(map[string]interface{}) matchRule, _ := mainRoute["match"].(string) if !strings.Contains(matchRule, "PathPrefix(`/forgejo/`)") { t.Errorf("expected PathPrefix /forgejo/ in main route, got %v", matchRule) } middlewares := mainRoute["middlewares"].([]interface{}) if len(middlewares) < 1 { t.Fatal("expected at least 1 middleware on main route") } mw := middlewares[0].(map[string]interface{}) if mw["name"] != "forgejo-basic-auth" { t.Errorf("expected forgejo-basic-auth middleware, got %v", mw["name"]) } } func TestForgejoBasicAuthMiddleware(t *testing.T) { docs := readForgejoDocuments(t) doc := findDoc(docs, "Middleware", "forgejo-basic-auth") if doc == nil { t.Fatal("missing forgejo-basic-auth Middleware") } meta := doc["metadata"].(map[string]interface{}) if meta["namespace"] != "dev-infra" { t.Errorf("expected namespace dev-infra, got %v", meta["namespace"]) } spec := doc["spec"].(map[string]interface{}) basicAuth := spec["basicAuth"].(map[string]interface{}) if basicAuth["secret"] != "forgejo-basic-auth-secret" { t.Errorf("expected secret forgejo-basic-auth-secret, got %v", basicAuth["secret"]) } } func TestForgejoTrailingSlashMiddleware(t *testing.T) { docs := readForgejoDocuments(t) doc := findDoc(docs, "Middleware", "forgejo-add-trailing-slash") if doc == nil { t.Fatal("missing forgejo-add-trailing-slash Middleware") } spec := doc["spec"].(map[string]interface{}) redirect := spec["redirectRegex"].(map[string]interface{}) if redirect["permanent"] != true { t.Error("expected permanent redirect") } }