package main import ( "os" "path/filepath" "testing" ) // ───────────────────────────────────────────── // usesCoreInternalLayout // ───────────────────────────────────────────── // Guard fires when internal/services/ exists. func TestUsesCoreInternalLayout_TrueWhenServicesExists(t *testing.T) { root := t.TempDir() if err := os.MkdirAll(filepath.Join(root, "internal", "services"), 0755); err != nil { t.Fatalf("mkdir: %v", err) } if !usesCoreInternalLayout(root) { t.Fatal("expected true when internal/services/ exists, got false") } } // Guard fires when internal/handlers/ exists. func TestUsesCoreInternalLayout_TrueWhenHandlersExists(t *testing.T) { root := t.TempDir() if err := os.MkdirAll(filepath.Join(root, "internal", "handlers"), 0755); err != nil { t.Fatalf("mkdir: %v", err) } if !usesCoreInternalLayout(root) { t.Fatal("expected true when internal/handlers/ exists, got false") } } // Guard fires when BOTH directories exist. func TestUsesCoreInternalLayout_TrueWhenBothExist(t *testing.T) { root := t.TempDir() if err := os.MkdirAll(filepath.Join(root, "internal", "services"), 0755); err != nil { t.Fatalf("mkdir services: %v", err) } if err := os.MkdirAll(filepath.Join(root, "internal", "handlers"), 0755); err != nil { t.Fatalf("mkdir handlers: %v", err) } if !usesCoreInternalLayout(root) { t.Fatal("expected true when both internal/services/ and internal/handlers/ exist") } } // Guard returns false when neither directory exists. func TestUsesCoreInternalLayout_FalseWhenNeitherExists(t *testing.T) { root := t.TempDir() // No internal/ tree at all — plugin-style flat layout. if usesCoreInternalLayout(root) { t.Fatal("expected false for empty root (no internal/{services,handlers}), got true") } } // Only the exact subdirectories count — internal/models/ alone is not enough. func TestUsesCoreInternalLayout_FalseForOtherInternalDirs(t *testing.T) { root := t.TempDir() if err := os.MkdirAll(filepath.Join(root, "internal", "models"), 0755); err != nil { t.Fatalf("mkdir: %v", err) } if usesCoreInternalLayout(root) { t.Fatal("expected false when only internal/models/ exists (not services/ or handlers/)") } } // A file named "services" is not a directory — should not satisfy the guard. func TestUsesCoreInternalLayout_FalseWhenServicesIsAFile(t *testing.T) { root := t.TempDir() if err := os.MkdirAll(filepath.Join(root, "internal"), 0755); err != nil { t.Fatalf("mkdir internal: %v", err) } // Create a regular file called "services" (not a directory). if err := os.WriteFile(filepath.Join(root, "internal", "services"), []byte("oops"), 0o644); err != nil { t.Fatalf("write file: %v", err) } if usesCoreInternalLayout(root) { t.Fatal("expected false when internal/services is a regular file, not a directory") } } // ───────────────────────────────────────────── // checkServiceStructure // ───────────────────────────────────────────── // Helper: builds a minimal fixture with internal/services/ (so the guard passes) // then writes a .go file at the given relative path inside root. func buildStructureFixture(t *testing.T, relGoPath, content string) string { t.Helper() root := t.TempDir() // Satisfy the guard: create internal/services/ if err := os.MkdirAll(filepath.Join(root, "internal", "services"), 0755); err != nil { t.Fatalf("mkdir internal/services: %v", err) } // Write the target Go file (create parent dirs as needed). abs := filepath.Join(root, filepath.FromSlash(relGoPath)) if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil { t.Fatalf("mkdir %s: %v", filepath.Dir(abs), err) } if err := os.WriteFile(abs, []byte(content), 0o644); err != nil { t.Fatalf("write %s: %v", relGoPath, err) } return root } // A type ending with "Service" at the root (outside internal/) must be flagged. func TestCheckServiceStructure_FlagsServiceTypeAtRoot(t *testing.T) { root := buildStructureFixture(t, "myservice.go", `package main type UserService struct{} `) vs := checkServiceStructure(root) if len(vs) == 0 { t.Fatal("expected violation for UserService outside internal/, got 0") } found := false for _, v := range vs { if v.rule == "service-location" { found = true } } if !found { t.Fatalf("expected rule='service-location', got violations: %#v", vs) } } // A type ending with "Handler" at the root (outside internal/) must be flagged. func TestCheckServiceStructure_FlagsHandlerTypeAtRoot(t *testing.T) { root := buildStructureFixture(t, "handler.go", `package main type PageHandler struct{} `) vs := checkServiceStructure(root) if len(vs) == 0 { t.Fatal("expected violation for PageHandler outside internal/, got 0") } found := false for _, v := range vs { if v.rule == "handler-location" { found = true } } if !found { t.Fatalf("expected rule='handler-location', got violations: %#v", vs) } } // A file under internal/services/ is SKIPPED entirely — even a type there generates no violation. func TestCheckServiceStructure_SkipsInternalServicesDir(t *testing.T) { root := buildStructureFixture(t, "internal/services/user_service.go", `package services type UserService struct{} `) vs := checkServiceStructure(root) if len(vs) != 0 { t.Fatalf("expected 0 violations for type in internal/services/, got %d: %#v", len(vs), vs) } } // A file under internal/handlers/ is SKIPPED entirely. func TestCheckServiceStructure_SkipsInternalHandlersDir(t *testing.T) { root := t.TempDir() // Provide internal/handlers/ as the guard directory. if err := os.MkdirAll(filepath.Join(root, "internal", "handlers"), 0755); err != nil { t.Fatalf("mkdir: %v", err) } abs := filepath.Join(root, "internal", "handlers", "page.go") if err := os.WriteFile(abs, []byte(`package handlers type PageHandler struct{} `), 0o644); err != nil { t.Fatalf("write: %v", err) } vs := checkServiceStructure(root) if len(vs) != 0 { t.Fatalf("expected 0 violations for type in internal/handlers/, got %d: %#v", len(vs), vs) } } // A type under internal/ (but NOT under internal/services/ or internal/handlers/) // is walked and the name check runs — however, since the relPath starts with // "internal/", the condition `!strings.HasPrefix(relPath, "internal/")` is false // and no violation is produced. Verify the no-violation outcome. func TestCheckServiceStructure_NoViolationForTypeInOtherInternalSubdir(t *testing.T) { root := buildStructureFixture(t, "internal/middleware/auth_service.go", `package middleware type AuthService struct{} `) vs := checkServiceStructure(root) if len(vs) != 0 { t.Fatalf("expected 0 violations for type in internal/middleware/ (relPath starts with internal/), got %d: %#v", len(vs), vs) } } // cmd/ files are skipped entirely — no violation even for types named *Service. func TestCheckServiceStructure_SkipsCmdDir(t *testing.T) { root := buildStructureFixture(t, "cmd/server/main.go", `package main type ServerService struct{} `) vs := checkServiceStructure(root) if len(vs) != 0 { t.Fatalf("expected 0 violations for type in cmd/ (skipped), got %d: %#v", len(vs), vs) } } // Test files (_test.go) are always skipped. func TestCheckServiceStructure_SkipsTestFiles(t *testing.T) { root := buildStructureFixture(t, "myservice_test.go", `package main type FakeUserService struct{} `) vs := checkServiceStructure(root) if len(vs) != 0 { t.Fatalf("expected 0 violations for type in _test.go (skipped), got %d: %#v", len(vs), vs) } } // Generated files (.connect.go, .pb.go, .gen.go) are skipped. func TestCheckServiceStructure_SkipsGeneratedFiles(t *testing.T) { root := buildStructureFixture(t, "rpc.pb.go", `package main type RPCService struct{} `) vs := checkServiceStructure(root) if len(vs) != 0 { t.Fatalf("expected 0 violations for .pb.go generated file, got %d: %#v", len(vs), vs) } } // api/ prefix files are skipped. func TestCheckServiceStructure_SkipsAPIDir(t *testing.T) { root := buildStructureFixture(t, "api/v1/handler.go", `package v1 type PageHandler struct{} `) vs := checkServiceStructure(root) if len(vs) != 0 { t.Fatalf("expected 0 violations for type in api/ (skipped), got %d: %#v", len(vs), vs) } } // When guard returns false (no internal layout), checkServiceStructure is a no-op. func TestCheckServiceStructure_NoOpWhenGuardFalse(t *testing.T) { // Root with NO internal/services or internal/handlers — guard will return false. root := t.TempDir() if err := os.WriteFile(filepath.Join(root, "svc.go"), []byte(`package main type UserService struct{} `), 0o644); err != nil { t.Fatalf("write: %v", err) } vs := checkServiceStructure(root) if len(vs) != 0 { t.Fatalf("expected 0 violations (guard=false, check skipped), got %d: %#v", len(vs), vs) } } // Clean case: valid placement — type in internal/services/ and internal/handlers/, // nothing misplaced anywhere. Zero violations. func TestCheckServiceStructure_CleanRepo(t *testing.T) { root := t.TempDir() // Satisfy guard. if err := os.MkdirAll(filepath.Join(root, "internal", "services"), 0755); err != nil { t.Fatalf("mkdir services: %v", err) } if err := os.MkdirAll(filepath.Join(root, "internal", "handlers"), 0755); err != nil { t.Fatalf("mkdir handlers: %v", err) } // Correctly placed service. if err := os.WriteFile(filepath.Join(root, "internal", "services", "user.go"), []byte(`package services type UserService struct { db interface{} } `), 0o644); err != nil { t.Fatalf("write user.go: %v", err) } // Correctly placed handler. if err := os.WriteFile(filepath.Join(root, "internal", "handlers", "page.go"), []byte(`package handlers type PageHandler struct { svc interface{} } `), 0o644); err != nil { t.Fatalf("write page.go: %v", err) } // A non-service/handler type at root — fine. if err := os.WriteFile(filepath.Join(root, "config.go"), []byte(`package main type Config struct { Debug bool } `), 0o644); err != nil { t.Fatalf("write config.go: %v", err) } vs := checkServiceStructure(root) if len(vs) != 0 { t.Fatalf("expected 0 violations for clean repo layout, got %d: %#v", len(vs), vs) } } // Snippet field encodes the type name in the expected format. func TestCheckServiceStructure_ViolationSnippetFormat(t *testing.T) { root := buildStructureFixture(t, "billing.go", `package main type BillingService struct{} `) vs := checkServiceStructure(root) if len(vs) == 0 { t.Fatal("expected at least one violation, got 0") } v := vs[0] if v.snippet != "type BillingService — should be in internal/services/" { t.Fatalf("unexpected snippet: %q", v.snippet) } if v.line == 0 { t.Error("expected non-zero line number in violation") } } // Multiple misplaced types in one file each produce their own violation. func TestCheckServiceStructure_MultipleViolationsInOneFile(t *testing.T) { root := buildStructureFixture(t, "mixed.go", `package main type OrderService struct{} type OrderHandler struct{} `) vs := checkServiceStructure(root) if len(vs) < 2 { t.Fatalf("expected at least 2 violations (1 service + 1 handler), got %d: %#v", len(vs), vs) } rules := map[string]bool{} for _, v := range vs { rules[v.rule] = true } if !rules["service-location"] { t.Error("expected 'service-location' violation") } if !rules["handler-location"] { t.Error("expected 'handler-location' violation") } }