diff --git a/internal/cli/command_body_test.go b/internal/cli/command_body_test.go new file mode 100644 index 0000000..4eace9d --- /dev/null +++ b/internal/cli/command_body_test.go @@ -0,0 +1,40 @@ +package cli + +import ( + "os" + "path/filepath" + "testing" +) + +func TestCommandBody(t *testing.T) { + bodyFile := filepath.Join(t.TempDir(), "body.md") + if err := os.WriteFile(bodyFile, []byte("body from file\n"), 0o600); err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + body string + bodyFile string + want string + wantErr bool + }{ + {name: "body flag", body: "inline body", want: "inline body"}, + {name: "neither", want: ""}, + {name: "body file", bodyFile: bodyFile, want: "body from file\n"}, + {name: "both", body: "inline", bodyFile: bodyFile, wantErr: true}, + {name: "missing file", bodyFile: filepath.Join(t.TempDir(), "nope.md"), wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := commandBody(tt.body, tt.bodyFile) + if (err != nil) != tt.wantErr { + t.Fatalf("error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.want { + t.Fatalf("got %q, want %q", got, tt.want) + } + }) + } +} diff --git a/internal/cli/config_test.go b/internal/cli/config_test.go new file mode 100644 index 0000000..7bae8c2 --- /dev/null +++ b/internal/cli/config_test.go @@ -0,0 +1,40 @@ +package cli + +import ( + "path/filepath" + "reflect" + "testing" +) + +func TestConfigSearchDirs(t *testing.T) { + xdg := t.TempDir() + home := t.TempDir() + + tests := []struct { + name string + xdg string + want []string + }{ + { + name: "xdg config home set", + xdg: xdg, + want: []string{filepath.Join(xdg, "tg"), "."}, + }, + { + name: "fall back to home", + xdg: "", + want: []string{filepath.Join(home, ".config", "tg"), "."}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", tt.xdg) + t.Setenv("HOME", home) + + if got := configSearchDirs(); !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/cli/rows_test.go b/internal/cli/rows_test.go new file mode 100644 index 0000000..79f2333 --- /dev/null +++ b/internal/cli/rows_test.go @@ -0,0 +1,173 @@ +package cli + +import ( + "testing" + + "github.com/alyraffauf/tg/tangled" +) + +func TestShortDate(t *testing.T) { + tests := []struct { + name string + timestamp string + want string + }{ + {name: "iso timestamp", timestamp: "2026-07-21T10:30:00Z", want: "2026-07-21"}, + {name: "date only", timestamp: "2026-07-21", want: "2026-07-21"}, + {name: "short string", timestamp: "2026", want: "2026"}, + {name: "empty", timestamp: "", want: ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := shortDate(tt.timestamp); got != tt.want { + t.Fatalf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestExtractDID(t *testing.T) { + tests := []struct { + name string + uri string + want string + }{ + {name: "record uri", uri: "at://did:plc:abc123/sh.tangled.repo.issue/3kdui", want: "did:plc:abc123"}, + {name: "bare did", uri: "did:plc:abc123", want: "did:plc:abc123"}, + {name: "trailing slash", uri: "at://did:plc:abc123/", want: "did:plc:abc123"}, + {name: "empty", uri: "", want: ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := extractDID(tt.uri); got != tt.want { + t.Fatalf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestExtractRKey(t *testing.T) { + tests := []struct { + name string + uri string + want string + }{ + {name: "record uri", uri: "at://did:plc:abc123/sh.tangled.repo.issue/3kdui", want: "3kdui"}, + {name: "bare rkey", uri: "3kdui", want: "3kdui"}, + {name: "trailing slash", uri: "at://did:plc:abc123/", want: ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := extractRKey(tt.uri); got != tt.want { + t.Fatalf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestDecodeIssue(t *testing.T) { + tests := []struct { + name string + raw string + want recordView + wantErr bool + }{ + { + name: "full record", + raw: `{"title":"Bug report","body":"details","createdAt":"2026-07-18T12:00:00Z"}`, + want: recordView{Title: "Bug report", Body: "details", CreatedAt: "2026-07-18T12:00:00Z"}, + }, + { + name: "title only", + raw: `{"title":"Bug report"}`, + want: recordView{Title: "Bug report"}, + }, + {name: "invalid json", raw: `{`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decodeIssue([]byte(tt.raw)) + if (err != nil) != tt.wantErr { + t.Fatalf("error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.want { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + }) + } +} + +func TestDecodePull(t *testing.T) { + tests := []struct { + name string + raw string + want recordView + wantErr bool + }{ + { + name: "full record", + raw: `{"title":"Add feature","body":"details","createdAt":"2026-07-18T12:00:00Z",` + + `"source":{"branch":"feature"},"target":{"branch":"main"}}`, + want: recordView{ + Title: "Add feature", + Body: "details", + CreatedAt: "2026-07-18T12:00:00Z", + SourceBranch: "feature", + TargetBranch: "main", + }, + }, + { + name: "title only", + raw: `{"title":"Add feature"}`, + want: recordView{Title: "Add feature"}, + }, + {name: "invalid json", raw: `{`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decodePull([]byte(tt.raw)) + if (err != nil) != tt.wantErr { + t.Fatalf("error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.want { + t.Fatalf("got %+v, want %+v", got, tt.want) + } + }) + } +} + +func TestFindByRKey(t *testing.T) { + items := []tangled.ListItem{ + {URI: "at://did:plc:abc123/sh.tangled.repo.issue/3kdui"}, + {URI: "at://did:plc:abc123/sh.tangled.repo.issue/9xyz"}, + } + + tests := []struct { + name string + rkey string + wantURI string + wantErr bool + }{ + {name: "found", rkey: "3kdui", wantURI: items[0].URI}, + {name: "found second", rkey: "9xyz", wantURI: items[1].URI}, + {name: "not found", rkey: "missing", wantErr: true}, + {name: "no partial match", rkey: "xyz", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := findByRKey(items, tt.rkey, "issue") + if (err != nil) != tt.wantErr { + t.Fatalf("error = %v, wantErr %v", err, tt.wantErr) + } + if err == nil && got.URI != tt.wantURI { + t.Fatalf("got %q, want %q", got.URI, tt.wantURI) + } + }) + } +} diff --git a/internal/cli/target_test.go b/internal/cli/target_test.go new file mode 100644 index 0000000..2391dc8 --- /dev/null +++ b/internal/cli/target_test.go @@ -0,0 +1,33 @@ +package cli + +import "testing" + +func TestParseHandleRepo(t *testing.T) { + tests := []struct { + name string + arg string + wantHandle string + wantRepo string + wantErr bool + }{ + {name: "handle and repo", arg: "aly.codes/tg", wantHandle: "aly.codes", wantRepo: "tg"}, + {name: "did handle", arg: "did:plc:abc123/tg", wantHandle: "did:plc:abc123", wantRepo: "tg"}, + {name: "repo containing slash", arg: "aly.codes/a/b", wantHandle: "aly.codes", wantRepo: "a/b"}, + {name: "no slash", arg: "tg", wantErr: true}, + {name: "empty handle", arg: "/tg", wantErr: true}, + {name: "empty repo", arg: "aly.codes/", wantErr: true}, + {name: "empty", arg: "", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handle, repo, err := parseHandleRepo(tt.arg) + if (err != nil) != tt.wantErr { + t.Fatalf("error = %v, wantErr %v", err, tt.wantErr) + } + if handle != tt.wantHandle || repo != tt.wantRepo { + t.Fatalf("got (%q, %q), want (%q, %q)", handle, repo, tt.wantHandle, tt.wantRepo) + } + }) + } +}