Files
kandev/apps/backend/internal/task/service/repository_discovery_test.go
T
Carlos Florêncio e1bce95bac feat: golang linter
2026-01-15 19:05:16 +00:00

292 lines
8.9 KiB
Go

package service
import (
"context"
"os"
"path/filepath"
"sort"
"strings"
"testing"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/events/bus"
"github.com/kandev/kandev/internal/task/repository"
)
func TestDiscoverLocalRepositoriesSkipsIgnoredRoots(t *testing.T) {
root := t.TempDir()
makeRepo(t, filepath.Join(root, "ProjectA"))
makeRepo(t, filepath.Join(root, "ProjectB"))
makeRepo(t, filepath.Join(root, "Library", "Caches", "mise", "python", "pyenv", "ProjectC"))
makeRepo(t, filepath.Join(root, ".cache", "ProjectD"))
makeRepo(t, filepath.Join(root, "node_modules", "ProjectE"))
makeRepo(t, filepath.Join(root, "ProjectA", "node_modules", "ProjectF"))
svc := newDiscoveryService(t, root)
result, err := svc.DiscoverLocalRepositories(context.Background(), "")
if err != nil {
t.Fatalf("DiscoverLocalRepositories error: %v", err)
}
paths := make([]string, 0, len(result.Repositories))
for _, repo := range result.Repositories {
paths = append(paths, repo.Path)
}
sort.Strings(paths)
expected := []string{
filepath.Join(root, "ProjectA"),
filepath.Join(root, "ProjectB"),
}
sort.Strings(expected)
if len(paths) != len(expected) {
t.Fatalf("expected %d repos, got %d: %#v", len(expected), len(paths), paths)
}
for i, path := range paths {
if path != expected[i] {
t.Fatalf("expected repo %q, got %q", expected[i], path)
}
}
}
func TestValidateLocalRepositoryPath(t *testing.T) {
root := t.TempDir()
svc := newDiscoveryService(t, root)
otherRoot := t.TempDir()
makeRepo(t, otherRoot)
outside, err := svc.ValidateLocalRepositoryPath(context.Background(), otherRoot)
if err != nil {
t.Fatalf("ValidateLocalRepositoryPath outside error: %v", err)
}
if outside.Allowed {
t.Fatalf("expected outside path to be disallowed")
}
if outside.Message != "Path is outside the allowed roots" {
t.Fatalf("expected outside message, got %q", outside.Message)
}
missingPath := filepath.Join(root, "missing")
missing, err := svc.ValidateLocalRepositoryPath(context.Background(), missingPath)
if err != nil {
t.Fatalf("ValidateLocalRepositoryPath missing error: %v", err)
}
if !missing.Allowed || missing.Exists {
t.Fatalf("expected missing path to be allowed and not exist")
}
if missing.Message != "Path does not exist" {
t.Fatalf("expected missing message, got %q", missing.Message)
}
filePath := filepath.Join(root, "file.txt")
if err := os.WriteFile(filePath, []byte("data"), 0o644); err != nil {
t.Fatalf("write file: %v", err)
}
fileResult, err := svc.ValidateLocalRepositoryPath(context.Background(), filePath)
if err != nil {
t.Fatalf("ValidateLocalRepositoryPath file error: %v", err)
}
if !fileResult.Allowed || !fileResult.Exists {
t.Fatalf("expected file path to be allowed and exist")
}
if fileResult.Message != "Path is not a directory" {
t.Fatalf("expected file message, got %q", fileResult.Message)
}
plainDir := filepath.Join(root, "plain")
if err := os.MkdirAll(plainDir, 0o755); err != nil {
t.Fatalf("mkdir plain dir: %v", err)
}
plainResult, err := svc.ValidateLocalRepositoryPath(context.Background(), plainDir)
if err != nil {
t.Fatalf("ValidateLocalRepositoryPath plain error: %v", err)
}
if plainResult.Message != "Not a git repository" {
t.Fatalf("expected plain message, got %q", plainResult.Message)
}
repoPath := filepath.Join(root, "repo")
makeRepo(t, repoPath)
repoResult, err := svc.ValidateLocalRepositoryPath(context.Background(), repoPath)
if err != nil {
t.Fatalf("ValidateLocalRepositoryPath repo error: %v", err)
}
if !repoResult.IsGitRepo || repoResult.DefaultBranch != "main" || repoResult.Message != "" {
t.Fatalf("expected git repo with main branch, got %+v", repoResult)
}
}
func TestNormalizeRootsDedupesAndCleans(t *testing.T) {
root := t.TempDir()
roots := []string{root, root + string(os.PathSeparator), "", root}
normalized := normalizeRoots(roots)
if len(normalized) != 1 {
t.Fatalf("expected 1 normalized root, got %d: %#v", len(normalized), normalized)
}
if normalized[0] != filepath.Clean(root) {
t.Fatalf("expected normalized root %q, got %q", filepath.Clean(root), normalized[0])
}
}
func TestReadGitDefaultBranch(t *testing.T) {
repoPath := t.TempDir()
gitDir := filepath.Join(repoPath, ".git")
if err := os.MkdirAll(gitDir, 0o755); err != nil {
t.Fatalf("mkdir git: %v", err)
}
headPath := filepath.Join(gitDir, "HEAD")
if err := os.WriteFile(headPath, []byte("ref: refs/heads/develop\n"), 0o644); err != nil {
t.Fatalf("write HEAD: %v", err)
}
branch, err := readGitDefaultBranch(repoPath)
if err != nil {
t.Fatalf("readGitDefaultBranch error: %v", err)
}
if branch != "develop" {
t.Fatalf("expected develop, got %q", branch)
}
if err := os.WriteFile(headPath, []byte("3a3f2d3b\n"), 0o644); err != nil {
t.Fatalf("write HEAD detached: %v", err)
}
branch, err = readGitDefaultBranch(repoPath)
if err != nil {
t.Fatalf("readGitDefaultBranch detached error: %v", err)
}
if branch != "HEAD" {
t.Fatalf("expected HEAD, got %q", branch)
}
if err := os.WriteFile(headPath, []byte("\n"), 0o644); err != nil {
t.Fatalf("write HEAD empty: %v", err)
}
if _, err := readGitDefaultBranch(repoPath); err == nil {
t.Fatalf("expected error for empty HEAD")
}
}
func TestResolveGitDir(t *testing.T) {
repoPath := t.TempDir()
gitDir := filepath.Join(repoPath, ".git")
if err := os.MkdirAll(gitDir, 0o755); err != nil {
t.Fatalf("mkdir git: %v", err)
}
resolved, err := resolveGitDir(repoPath)
if err != nil {
t.Fatalf("resolveGitDir dir error: %v", err)
}
if resolved != gitDir {
t.Fatalf("expected git dir %q, got %q", gitDir, resolved)
}
altRepo := t.TempDir()
altGit := filepath.Join(altRepo, "gitdir")
if err := os.MkdirAll(altGit, 0o755); err != nil {
t.Fatalf("mkdir alt git: %v", err)
}
gitRef := filepath.Join(altRepo, ".git")
relPath := filepath.Join("gitdir")
if err := os.WriteFile(gitRef, []byte("gitdir: "+relPath+"\n"), 0o644); err != nil {
t.Fatalf("write gitdir ref: %v", err)
}
resolved, err = resolveGitDir(altRepo)
if err != nil {
t.Fatalf("resolveGitDir file error: %v", err)
}
expected := filepath.Clean(filepath.Join(altRepo, relPath))
if resolved != expected {
t.Fatalf("expected git dir %q, got %q", expected, resolved)
}
}
func TestListGitBranches(t *testing.T) {
repoPath := t.TempDir()
gitDir := filepath.Join(repoPath, ".git")
if err := os.MkdirAll(gitDir, 0o755); err != nil {
t.Fatalf("mkdir git: %v", err)
}
writeRef(t, filepath.Join(gitDir, "refs", "heads", "main"))
writeRef(t, filepath.Join(gitDir, "refs", "heads", "feature", "test"))
writeRef(t, filepath.Join(gitDir, "refs", "remotes", "origin", "main"))
writeRef(t, filepath.Join(gitDir, "refs", "remotes", "origin", "HEAD"))
writeRef(t, filepath.Join(gitDir, "refs", "remotes", "upstream", "dev"))
packedRefs := strings.Join([]string{
"# pack-refs with: peeled fully-peeled",
"deadbeef refs/heads/packed",
"cafebabe refs/remotes/origin/packed-remote",
"^deadbeef",
"",
}, "\n")
if err := os.WriteFile(filepath.Join(gitDir, "packed-refs"), []byte(packedRefs), 0o644); err != nil {
t.Fatalf("write packed-refs: %v", err)
}
branches, err := listGitBranches(repoPath)
if err != nil {
t.Fatalf("listGitBranches error: %v", err)
}
expected := []Branch{
{Name: "feature/test", Type: "local"},
{Name: "main", Type: "local"},
{Name: "packed", Type: "local"},
{Name: "main", Type: "remote", Remote: "origin"},
{Name: "packed-remote", Type: "remote", Remote: "origin"},
{Name: "dev", Type: "remote", Remote: "upstream"},
}
if len(branches) != len(expected) {
t.Fatalf("expected %d branches, got %d: %#v", len(expected), len(branches), branches)
}
for i, branch := range branches {
if branch != expected[i] {
t.Fatalf("expected branch %#v, got %#v", expected[i], branch)
}
}
}
func makeRepo(t *testing.T, path string) {
t.Helper()
gitDir := filepath.Join(path, ".git")
if err := os.MkdirAll(gitDir, 0o755); err != nil {
t.Fatalf("mkdir repo: %v", err)
}
headPath := filepath.Join(gitDir, "HEAD")
if err := os.WriteFile(headPath, []byte("ref: refs/heads/main\n"), 0o644); err != nil {
t.Fatalf("write HEAD: %v", err)
}
}
func writeRef(t *testing.T, path string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir ref dir: %v", err)
}
if err := os.WriteFile(path, []byte("0000000\n"), 0o644); err != nil {
t.Fatalf("write ref: %v", err)
}
}
func newDiscoveryService(t *testing.T, root string) *Service {
t.Helper()
tmpDir := t.TempDir()
repo, err := repository.NewSQLiteRepository(tmpDir + "/test.db")
if err != nil {
t.Fatalf("failed to create test repository: %v", err)
}
t.Cleanup(func() {
if err := repo.Close(); err != nil {
t.Errorf("failed to close repo: %v", err)
}
})
log, _ := logger.NewLogger(logger.LoggingConfig{Level: "error", Format: "json", OutputPath: "stdout"})
eventBus := bus.NewMemoryEventBus(log)
return NewService(repo, eventBus, log, RepositoryDiscoveryConfig{
Roots: []string{root},
MaxDepth: 6,
})
}