feat: update deps (go-github v84, githubevents v2.15, signalcli) and add format tests

- Bump go-github from v70 to v84
- Bump githubevents from v2.2.0 to v2.15.2
- Update signalcli to latest
- Update Go toolchain to 1.26.2
- Add comprehensive tests for all format functions
- Add tests for firstLine, truncate, splitMessage helpers
- Add edge case tests (singular commit, closed issue body suppression)
This commit is contained in:
2026-04-13 09:11:50 +00:00
parent 816ec8fd6b
commit cffa7821e2
5 changed files with 522 additions and 15 deletions

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"strings"
"github.com/google/go-github/v70/github"
"github.com/google/go-github/v84/github"
)
// formatPush formats a push event into a Signal message.

507
format_test.go Normal file
View File

@@ -0,0 +1,507 @@
package main
import (
"testing"
"github.com/google/go-github/v84/github"
)
func strPtr(s string) *string { return &s }
func intPtr(i int) *int { return &i }
func TestFormatPush(t *testing.T) {
event := &github.PushEvent{
Ref: strPtr("refs/heads/main"),
Repo: &github.PushEventRepository{
FullName: strPtr("taigrr/example"),
},
Pusher: &github.CommitAuthor{
Name: strPtr("tai"),
},
Commits: []*github.HeadCommit{
{
ID: strPtr("abc1234567890"),
Message: strPtr("feat: initial commit"),
},
{
ID: strPtr("def4567890123"),
Message: strPtr("fix: typo\n\nLonger description here"),
},
},
}
got := formatPush(event)
if got == "" {
t.Fatal("formatPush returned empty string")
}
// Check key parts are present
checks := []string{
"[taigrr/example]",
"tai pushed 2 commits to main",
"abc1234",
"feat: initial commit",
"def4567",
"fix: typo",
}
for _, c := range checks {
if !contains(got, c) {
t.Errorf("formatPush missing %q in output:\n%s", c, got)
}
}
}
func TestFormatPushSingular(t *testing.T) {
event := &github.PushEvent{
Ref: strPtr("refs/heads/dev"),
Repo: &github.PushEventRepository{
FullName: strPtr("taigrr/repo"),
},
Pusher: &github.CommitAuthor{
Name: strPtr("tai"),
},
Commits: []*github.HeadCommit{
{
ID: strPtr("aaa1111222233"),
Message: strPtr("docs: update readme"),
},
},
}
got := formatPush(event)
if !contains(got, "1 commit to dev") {
t.Errorf("expected singular 'commit', got:\n%s", got)
}
}
func TestFormatIssue(t *testing.T) {
event := &github.IssuesEvent{
Action: strPtr("opened"),
Repo: &github.Repository{
FullName: strPtr("taigrr/example"),
},
Sender: &github.User{
Login: strPtr("contributor"),
},
Issue: &github.Issue{
Number: intPtr(42),
Title: strPtr("Bug: something broken"),
HTMLURL: strPtr("https://github.com/taigrr/example/issues/42"),
Body: strPtr("Steps to reproduce..."),
},
}
got := formatIssue(event)
checks := []string{
"[taigrr/example]",
"contributor",
"opened",
"#42",
"Bug: something broken",
"https://github.com/taigrr/example/issues/42",
"Steps to reproduce...",
}
for _, c := range checks {
if !contains(got, c) {
t.Errorf("formatIssue missing %q in output:\n%s", c, got)
}
}
}
func TestFormatIssueComment(t *testing.T) {
event := &github.IssueCommentEvent{
Repo: &github.Repository{
FullName: strPtr("taigrr/example"),
},
Sender: &github.User{
Login: strPtr("reviewer"),
},
Issue: &github.Issue{
Number: intPtr(10),
Title: strPtr("Feature request"),
},
Comment: &github.IssueComment{
Body: strPtr("Looks good to me!"),
HTMLURL: strPtr("https://github.com/taigrr/example/issues/10#comment-1"),
},
}
got := formatIssueComment(event)
checks := []string{
"[taigrr/example]",
"reviewer",
"#10",
"Feature request",
"Looks good to me!",
}
for _, c := range checks {
if !contains(got, c) {
t.Errorf("formatIssueComment missing %q in output:\n%s", c, got)
}
}
}
func TestFormatPR(t *testing.T) {
event := &github.PullRequestEvent{
Action: strPtr("opened"),
Repo: &github.Repository{
FullName: strPtr("taigrr/example"),
},
Sender: &github.User{
Login: strPtr("author"),
},
PullRequest: &github.PullRequest{
Number: intPtr(5),
Title: strPtr("Add feature X"),
HTMLURL: strPtr("https://github.com/taigrr/example/pull/5"),
Body: strPtr("This PR adds feature X"),
},
}
got := formatPR(event)
checks := []string{
"[taigrr/example]",
"author",
"opened",
"PR #5",
"Add feature X",
"This PR adds feature X",
}
for _, c := range checks {
if !contains(got, c) {
t.Errorf("formatPR missing %q in output:\n%s", c, got)
}
}
}
func TestFormatPRReview(t *testing.T) {
event := &github.PullRequestReviewEvent{
Repo: &github.Repository{
FullName: strPtr("taigrr/example"),
},
Sender: &github.User{
Login: strPtr("reviewer"),
},
PullRequest: &github.PullRequest{
Number: intPtr(5),
Title: strPtr("Add feature X"),
},
Review: &github.PullRequestReview{
State: strPtr("approved"),
Body: strPtr("Ship it!"),
HTMLURL: strPtr("https://github.com/taigrr/example/pull/5#pullrequestreview-1"),
},
}
got := formatPRReview(event)
checks := []string{
"[taigrr/example]",
"reviewer",
"approved",
"PR #5",
"Ship it!",
}
for _, c := range checks {
if !contains(got, c) {
t.Errorf("formatPRReview missing %q in output:\n%s", c, got)
}
}
}
func TestFormatPRReviewComment(t *testing.T) {
event := &github.PullRequestReviewCommentEvent{
Repo: &github.Repository{
FullName: strPtr("taigrr/example"),
},
Sender: &github.User{
Login: strPtr("reviewer"),
},
PullRequest: &github.PullRequest{
Number: intPtr(5),
Title: strPtr("Add feature X"),
},
Comment: &github.PullRequestComment{
Body: strPtr("Nit: rename this variable"),
HTMLURL: strPtr("https://github.com/taigrr/example/pull/5#discussion_r1"),
},
}
got := formatPRReviewComment(event)
checks := []string{
"[taigrr/example]",
"reviewer",
"PR #5",
"Nit: rename this variable",
}
for _, c := range checks {
if !contains(got, c) {
t.Errorf("formatPRReviewComment missing %q in output:\n%s", c, got)
}
}
}
func TestFormatRelease(t *testing.T) {
event := &github.ReleaseEvent{
Action: strPtr("published"),
Repo: &github.Repository{
FullName: strPtr("taigrr/example"),
},
Sender: &github.User{
Login: strPtr("tai"),
},
Release: &github.RepositoryRelease{
TagName: strPtr("v1.0.0"),
HTMLURL: strPtr("https://github.com/taigrr/example/releases/tag/v1.0.0"),
},
}
got := formatRelease(event)
checks := []string{
"[taigrr/example]",
"tai",
"published",
"v1.0.0",
}
for _, c := range checks {
if !contains(got, c) {
t.Errorf("formatRelease missing %q in output:\n%s", c, got)
}
}
}
func TestFormatStar(t *testing.T) {
event := &github.StarEvent{
Action: strPtr("created"),
Repo: &github.Repository{
FullName: strPtr("taigrr/example"),
StargazersCount: intPtr(100),
},
Sender: &github.User{
Login: strPtr("fan"),
},
}
got := formatStar(event)
if !contains(got, "fan") || !contains(got, "starred") || !contains(got, "100") {
t.Errorf("formatStar unexpected output:\n%s", got)
}
// Test unstar
event.Action = strPtr("deleted")
got = formatStar(event)
if !contains(got, "unstarred") {
t.Errorf("formatStar deleted should say 'unstarred', got:\n%s", got)
}
}
func TestFormatFork(t *testing.T) {
event := &github.ForkEvent{
Repo: &github.Repository{
FullName: strPtr("taigrr/example"),
},
Forkee: &github.Repository{
FullName: strPtr("user/example"),
},
Sender: &github.User{
Login: strPtr("user"),
},
}
got := formatFork(event)
checks := []string{"taigrr/example", "user", "forked", "user/example"}
for _, c := range checks {
if !contains(got, c) {
t.Errorf("formatFork missing %q in output:\n%s", c, got)
}
}
}
func TestFormatWorkflowRun(t *testing.T) {
event := &github.WorkflowRunEvent{
Action: strPtr("completed"),
Repo: &github.Repository{
FullName: strPtr("taigrr/example"),
},
WorkflowRun: &github.WorkflowRun{
Name: strPtr("CI"),
Conclusion: strPtr("success"),
HeadBranch: strPtr("main"),
HTMLURL: strPtr("https://github.com/taigrr/example/actions/runs/1"),
},
}
got := formatWorkflowRun(event)
if !contains(got, "✅") || !contains(got, "CI") || !contains(got, "success") {
t.Errorf("formatWorkflowRun success unexpected output:\n%s", got)
}
// Test failure
event.WorkflowRun.Conclusion = strPtr("failure")
got = formatWorkflowRun(event)
if !contains(got, "❌") {
t.Errorf("formatWorkflowRun failure should have ❌, got:\n%s", got)
}
// Test cancelled
event.WorkflowRun.Conclusion = strPtr("cancelled")
got = formatWorkflowRun(event)
if !contains(got, "⚠️") {
t.Errorf("formatWorkflowRun cancelled should have ⚠️, got:\n%s", got)
}
// Test non-completed action returns empty
event.Action = strPtr("requested")
got = formatWorkflowRun(event)
if got != "" {
t.Errorf("formatWorkflowRun non-completed should return empty, got:\n%s", got)
}
}
func TestFormatCreate(t *testing.T) {
event := &github.CreateEvent{
Repo: &github.Repository{
FullName: strPtr("taigrr/example"),
},
Sender: &github.User{
Login: strPtr("tai"),
},
RefType: strPtr("branch"),
Ref: strPtr("feature-x"),
}
got := formatCreate(event)
if !contains(got, "created") || !contains(got, "branch") || !contains(got, "feature-x") {
t.Errorf("formatCreate unexpected output:\n%s", got)
}
}
func TestFormatDelete(t *testing.T) {
event := &github.DeleteEvent{
Repo: &github.Repository{
FullName: strPtr("taigrr/example"),
},
Sender: &github.User{
Login: strPtr("tai"),
},
RefType: strPtr("tag"),
Ref: strPtr("v0.1.0"),
}
got := formatDelete(event)
if !contains(got, "deleted") || !contains(got, "tag") || !contains(got, "v0.1.0") {
t.Errorf("formatDelete unexpected output:\n%s", got)
}
}
func TestFirstLine(t *testing.T) {
tests := []struct {
input string
want string
}{
{"single line", "single line"},
{"first\nsecond\nthird", "first"},
{"", ""},
{"trailing\n", "trailing"},
}
for _, tt := range tests {
if got := firstLine(tt.input); got != tt.want {
t.Errorf("firstLine(%q) = %q, want %q", tt.input, got, tt.want)
}
}
}
func TestTruncate(t *testing.T) {
tests := []struct {
input string
maxLen int
want string
}{
{"short", 10, "short"},
{"exactly10!", 10, "exactly10!"},
{"this is too long", 10, "this is to..."},
{"", 5, ""},
}
for _, tt := range tests {
if got := truncate(tt.input, tt.maxLen); got != tt.want {
t.Errorf("truncate(%q, %d) = %q, want %q", tt.input, tt.maxLen, got, tt.want)
}
}
}
func TestSplitMessage(t *testing.T) {
// Short message — no split
short := "hello"
chunks := splitMessage(short)
if len(chunks) != 1 || chunks[0] != short {
t.Errorf("splitMessage short: got %v", chunks)
}
// Exactly at limit
exact := string(make([]byte, maxMessageLen))
for i := range exact {
exact = exact[:i] + "a" + exact[i+1:]
}
chunks = splitMessage(exact)
if len(chunks) != 1 {
t.Errorf("splitMessage exact: got %d chunks, want 1", len(chunks))
}
// Over limit — should split
long := make([]byte, maxMessageLen+500)
for i := range long {
long[i] = 'x'
}
// Insert a newline near the boundary for clean split
long[maxMessageLen-10] = '\n'
chunks = splitMessage(string(long))
if len(chunks) < 2 {
t.Errorf("splitMessage long: expected 2+ chunks, got %d", len(chunks))
}
// Verify all content is preserved
total := 0
for _, c := range chunks {
total += len(c)
}
// Account for whitespace trimming
if total < maxMessageLen {
t.Errorf("splitMessage long: total content %d seems too small", total)
}
}
func TestFormatIssueClosedNoBody(t *testing.T) {
event := &github.IssuesEvent{
Action: strPtr("closed"),
Repo: &github.Repository{
FullName: strPtr("taigrr/example"),
},
Sender: &github.User{
Login: strPtr("tai"),
},
Issue: &github.Issue{
Number: intPtr(1),
Title: strPtr("Old bug"),
HTMLURL: strPtr("https://github.com/taigrr/example/issues/1"),
Body: strPtr("Some body that should not appear"),
},
}
got := formatIssue(event)
// Body should NOT be included for non-opened actions
if contains(got, "Some body that should not appear") {
t.Errorf("formatIssue closed should not include body, got:\n%s", got)
}
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
(len(substr) > 0 && containsStr(s, substr)))
}
func containsStr(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}

10
go.mod
View File

@@ -1,18 +1,18 @@
module github.com/taigrr/github-to-signal
go 1.26.1
go 1.26.2
require (
github.com/cbrgm/githubevents/v2 v2.2.0
github.com/google/go-github/v70 v70.0.0
github.com/cbrgm/githubevents/v2 v2.15.2
github.com/google/go-github/v84 v84.0.0
github.com/taigrr/jety v0.4.0
github.com/taigrr/signalcli v0.0.0-20260301154901-6aed09f3f2af
github.com/taigrr/signalcli v0.0.0-20260412082710-b926f174d525
)
require (
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sync v0.20.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

16
go.sum
View File

@@ -1,22 +1,22 @@
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/cbrgm/githubevents/v2 v2.2.0 h1:nttKcls+vGtq+osxirM1qpY287hkXyJmTjHWHjQT1/A=
github.com/cbrgm/githubevents/v2 v2.2.0/go.mod h1:3J4oJkVO5NlZNEtEyUb8B7sW7+q8OZgHdTBvw9dPsMs=
github.com/cbrgm/githubevents/v2 v2.15.2 h1:0sNunNkNzovXKez91KzWZYWlqzXTl9waj6HN5OPOiKw=
github.com/cbrgm/githubevents/v2 v2.15.2/go.mod h1:XtEZTSgv0iRx1BO6E3xXNp2iwAu84CmDYsKrMHztDE8=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v70 v70.0.0 h1:/tqCp5KPrcvqCc7vIvYyFYTiCGrYvaWoYMGHSQbo55o=
github.com/google/go-github/v70 v70.0.0/go.mod h1:xBUZgo8MI3lUL/hwxl3hlceJW1U8MVnXP3zUyI+rhQY=
github.com/google/go-github/v84 v84.0.0 h1:I/0Xn5IuChMe8TdmI2bbim5nyhaRFJ7DEdzmD2w+yVA=
github.com/google/go-github/v84 v84.0.0/go.mod h1:WwYL1z1ajRdlaPszjVu/47x1L0PXukJBn73xsiYrRRQ=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/taigrr/jety v0.4.0 h1:BECC3r3CdQOxN/OdJpJ1VFH6DCJnmNby4vxRwL2wcZQ=
github.com/taigrr/jety v0.4.0/go.mod h1:Z8O3yHvOIv0O+KTadzHl58/gfAtLwNo8FlsL2JwpaKA=
github.com/taigrr/signalcli v0.0.0-20260301154901-6aed09f3f2af h1:qavmWkSyV3NeiZ+HHVj1I/6c8VQosUKFV1SfN1X/4DM=
github.com/taigrr/signalcli v0.0.0-20260301154901-6aed09f3f2af/go.mod h1:Jgly+nAwowk5O5ZUXSAAxCOVK9nlsAvPIfdNg5kXWSM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
github.com/taigrr/signalcli v0.0.0-20260412082710-b926f174d525 h1:tK6cU4iaSSp/HwsFOfeaVJfviMWBBh/V92AV8P3LAYw=
github.com/taigrr/signalcli v0.0.0-20260412082710-b926f174d525/go.mod h1:dgF5U1lF8rdr4PsrDyvv/jMEYJ1OYfjRNz9AgGRh7aU=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -12,7 +12,7 @@ import (
"strings"
"github.com/cbrgm/githubevents/v2/githubevents"
"github.com/google/go-github/v70/github"
"github.com/google/go-github/v84/github"
"github.com/taigrr/signalcli"
)