diff --git a/internal/git/git_test.go b/internal/git/git_test.go index fd5ee80..df523a0 100644 --- a/internal/git/git_test.go +++ b/internal/git/git_test.go @@ -3,8 +3,10 @@ package git_test import ( "path/filepath" "testing" + "time" "github.com/alecthomas/assert/v2" + "github.com/go-git/go-git/v5/plumbing/protocol/packp" "go.jolheiser.com/ugit/internal/git" ) @@ -43,3 +45,232 @@ func TestRepo(t *testing.T) { assert.NoError(t, err, "should not error when getting existing repo") assert.False(t, repo.Meta.Private, "repo should be public after saving meta") } + +func TestPathExists(t *testing.T) { + tmp := t.TempDir() + exists, err := git.PathExists(tmp) + assert.NoError(t, err) + assert.True(t, exists) + + doesNotExist := filepath.Join(tmp, "does-not-exist") + exists, err = git.PathExists(doesNotExist) + assert.NoError(t, err) + assert.False(t, exists) +} + +func TestRepoMetaUpdate(t *testing.T) { + original := git.RepoMeta{ + Description: "Original description", + Private: true, + Tags: git.TagSet{"tag1": struct{}{}, "tag2": struct{}{}}, + } + + update := git.RepoMeta{ + Description: "Updated description", + Private: false, + Tags: git.TagSet{"tag3": struct{}{}}, + } + + err := original.Update(update) + assert.NoError(t, err) + + assert.Equal(t, "Updated description", original.Description) + assert.False(t, original.Private) + assert.Equal(t, []string{"tag1", "tag2", "tag3"}, original.Tags.Slice()) +} + +func TestFileInfoName(t *testing.T) { + testCases := []struct { + path string + expected string + }{ + {path: "file.txt", expected: "file.txt"}, + {path: "dir/file.txt", expected: "file.txt"}, + {path: "nested/path/to/file.go", expected: "file.go"}, + {path: "README.md", expected: "README.md"}, + } + + for _, tc := range testCases { + t.Run(tc.path, func(t *testing.T) { + fi := git.FileInfo{Path: tc.path} + assert.Equal(t, tc.expected, fi.Name()) + }) + } +} + +func TestCommitSummaryAndDetails(t *testing.T) { + testCases := []struct { + message string + expectedSummary string + expectedDetails string + }{ + { + message: "Simple commit message", + expectedSummary: "Simple commit message", + expectedDetails: "", + }, + { + message: "Add feature X\n\nThis commit adds feature X\nWith multiple details\nAcross multiple lines", + expectedSummary: "Add feature X", + expectedDetails: "\nThis commit adds feature X\nWith multiple details\nAcross multiple lines", + }, + { + message: "Fix bug\n\nDetailed explanation", + expectedSummary: "Fix bug", + expectedDetails: "\nDetailed explanation", + }, + } + + for _, tc := range testCases { + t.Run(tc.message, func(t *testing.T) { + commit := git.Commit{ + SHA: "abcdef1234567890", + Message: tc.message, + Signature: "", + Author: "Test User", + Email: "test@example.com", + When: time.Now(), + } + + assert.Equal(t, tc.expectedSummary, commit.Summary()) + assert.Equal(t, tc.expectedDetails, commit.Details()) + }) + } +} + +func TestCommitShort(t *testing.T) { + commit := git.Commit{ + SHA: "abcdef1234567890abcdef1234567890", + } + + assert.Equal(t, "abcdef12", commit.Short()) +} + +func TestCommitFilePath(t *testing.T) { + testCases := []struct { + name string + fromPath string + toPath string + expected string + }{ + { + name: "to path preferred", + fromPath: "old/path.txt", + toPath: "new/path.txt", + expected: "new/path.txt", + }, + { + name: "fallback to from path", + fromPath: "deleted/file.txt", + toPath: "", + expected: "deleted/file.txt", + }, + { + name: "both paths empty", + fromPath: "", + toPath: "", + expected: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cf := git.CommitFile{ + From: git.CommitFileEntry{Path: tc.fromPath}, + To: git.CommitFileEntry{Path: tc.toPath}, + } + assert.Equal(t, tc.expected, cf.Path()) + }) + } +} + +func TestRepoName(t *testing.T) { + tmp := t.TempDir() + + repoName := "testrepo" + err := git.EnsureRepo(tmp, repoName+".git") + assert.NoError(t, err) + + repo, err := git.NewRepo(tmp, repoName) + assert.NoError(t, err) + assert.Equal(t, repoName, repo.Name()) + + repoName2 := "test-repo-with-hyphens" + err = git.EnsureRepo(tmp, repoName2+".git") + assert.NoError(t, err) + + repo2, err := git.NewRepo(tmp, repoName2) + assert.NoError(t, err) + assert.Equal(t, repoName2, repo2.Name()) +} + +func TestHandlePushOptions(t *testing.T) { + tmp := t.TempDir() + err := git.EnsureRepo(tmp, "test.git") + assert.NoError(t, err) + + repo, err := git.NewRepo(tmp, "test") + assert.NoError(t, err) + + opts := []*packp.Option{ + {Key: "description", Value: "New description"}, + } + err = git.HandlePushOptions(repo, opts) + assert.NoError(t, err) + assert.Equal(t, "New description", repo.Meta.Description) + + opts = []*packp.Option{ + {Key: "private", Value: "false"}, + } + err = git.HandlePushOptions(repo, opts) + assert.NoError(t, err) + assert.False(t, repo.Meta.Private) + + repo.Meta.Private = true + opts = []*packp.Option{ + {Key: "private", Value: "invalid"}, + } + err = git.HandlePushOptions(repo, opts) + assert.NoError(t, err) + assert.True(t, repo.Meta.Private) + + opts = []*packp.Option{ + {Key: "tags", Value: "tag1,tag2"}, + } + err = git.HandlePushOptions(repo, opts) + assert.NoError(t, err) + + opts = []*packp.Option{ + {Key: "description", Value: "Combined update"}, + {Key: "private", Value: "true"}, + } + err = git.HandlePushOptions(repo, opts) + assert.NoError(t, err) + assert.Equal(t, "Combined update", repo.Meta.Description) + assert.True(t, repo.Meta.Private) +} + +func TestRepoPath(t *testing.T) { + tmp := t.TempDir() + err := git.EnsureRepo(tmp, "test.git") + assert.NoError(t, err) + + repo, err := git.NewRepo(tmp, "test") + assert.NoError(t, err) + + expected := filepath.Join(tmp, "test.git") + assert.Equal(t, expected, repo.Path()) +} + +func TestEnsureJSONFile(t *testing.T) { + tmp := t.TempDir() + err := git.EnsureRepo(tmp, "test.git") + assert.NoError(t, err) + + repo, err := git.NewRepo(tmp, "test") + assert.NoError(t, err) + + assert.True(t, repo.Meta.Private, "default repo should be private") + assert.Equal(t, "", repo.Meta.Description, "default description should be empty") + assert.Equal(t, 0, len(repo.Meta.Tags), "default tags should be empty") +} diff --git a/internal/git/repo.go b/internal/git/repo.go index 1d19a43..2b9eff8 100644 --- a/internal/git/repo.go +++ b/internal/git/repo.go @@ -57,6 +57,9 @@ func NewRepo(dir, name string) (*Repo, error) { if err := json.NewDecoder(fi).Decode(&r.Meta); err != nil { return nil, err } + if r.Meta.Tags == nil { + r.Meta.Tags = make(TagSet) + } return r, nil } diff --git a/internal/http/httperr/httperr_test.go b/internal/http/httperr/httperr_test.go new file mode 100644 index 0000000..562e564 --- /dev/null +++ b/internal/http/httperr/httperr_test.go @@ -0,0 +1,122 @@ +package httperr_test + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/alecthomas/assert/v2" + "go.jolheiser.com/ugit/internal/http/httperr" +) + +func successHandler(w http.ResponseWriter, r *http.Request) error { + w.WriteHeader(http.StatusOK) + return nil +} + +func errorHandler(w http.ResponseWriter, r *http.Request) error { + return errors.New("test error") +} + +func statusErrorHandler(status int) func(w http.ResponseWriter, r *http.Request) error { + return func(w http.ResponseWriter, r *http.Request) error { + return httperr.Status(errors.New("test error"), status) + } +} + +func TestHandler_Success(t *testing.T) { + handler := httperr.Handler(successHandler) + + req := httptest.NewRequest("GET", "/", nil) + recorder := httptest.NewRecorder() + + handler.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusOK, recorder.Code) +} + +func TestHandler_Error(t *testing.T) { + handler := httperr.Handler(errorHandler) + + req := httptest.NewRequest("GET", "/", nil) + recorder := httptest.NewRecorder() + + handler.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusInternalServerError, recorder.Code) +} + +func TestHandler_StatusError(t *testing.T) { + testCases := []struct { + name string + status int + expectedStatus int + }{ + { + name: "not found", + status: http.StatusNotFound, + expectedStatus: http.StatusNotFound, + }, + { + name: "bad request", + status: http.StatusBadRequest, + expectedStatus: http.StatusBadRequest, + }, + { + name: "unauthorized", + status: http.StatusUnauthorized, + expectedStatus: http.StatusUnauthorized, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + handler := httperr.Handler(statusErrorHandler(tc.status)) + + req := httptest.NewRequest("GET", "/", nil) + recorder := httptest.NewRecorder() + + handler.ServeHTTP(recorder, req) + + assert.Equal(t, tc.expectedStatus, recorder.Code) + }) + } +} + +type unwrapper interface { + Unwrap() error +} + +func TestError(t *testing.T) { + originalErr := errors.New("original error") + httpErr := httperr.Error(originalErr) + + assert.Equal(t, originalErr.Error(), httpErr.Error()) + + unwrapper, ok := any(httpErr).(unwrapper) + assert.True(t, ok) + assert.Equal(t, originalErr, unwrapper.Unwrap()) +} + +func TestStatus(t *testing.T) { + originalErr := errors.New("original error") + httpErr := httperr.Status(originalErr, http.StatusNotFound) + + assert.Equal(t, originalErr.Error(), httpErr.Error()) + + unwrapper, ok := any(httpErr).(unwrapper) + assert.True(t, ok) + assert.Equal(t, originalErr, unwrapper.Unwrap()) + + handler := httperr.Handler(func(w http.ResponseWriter, r *http.Request) error { + return httpErr + }) + + req := httptest.NewRequest("GET", "/", nil) + recorder := httptest.NewRecorder() + + handler.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusNotFound, recorder.Code) +}