feat(releaser): release v1.5.0 — Node.js, multi-module Maven, configurable bump rules
- Add internal/node package: reads/writes package.json version (single or multi-path via node.package_json / node.package_jsons) - Add maven.pom_paths support: update multiple pom.xml files in one release commit; pom_paths overrides pom_path; --pom flag clears pom_paths - Add git.bump_rules config: per-type control of which version component bumps (breaking/feat/fix accept "patch" or "minor"); wired through version.Next() as a new sixth parameter - Extract injectable function vars (absPath, gitAllCommits, gitCommitsSince, gitCommitFiles) to enable error-path testing without interfaces - Achieve 100% per-package statement coverage across all 12 packages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
package ghclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
c := New("tok", "owner/repo")
|
||||
if c.token != "tok" || c.repo != "owner/repo" || c.httpClient == nil {
|
||||
t.Fatalf("New fields: token=%q repo=%q httpClient=%v", c.token, c.repo, c.httpClient)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateReleaseSuccess(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("method = %q, want POST", r.Method)
|
||||
}
|
||||
if !strings.HasSuffix(r.URL.Path, "/releases") {
|
||||
t.Errorf("path = %q, want .../releases", r.URL.Path)
|
||||
}
|
||||
if r.Header.Get("Authorization") != "Bearer test-token" {
|
||||
t.Errorf("Authorization = %q", r.Header.Get("Authorization"))
|
||||
}
|
||||
var req struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Name string `json:"name"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
if req.TagName != "v1.0.0" {
|
||||
t.Errorf("tag_name = %q, want v1.0.0", req.TagName)
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
io.WriteString(w, `{}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New("test-token", "owner/repo")
|
||||
c.httpClient = srv.Client()
|
||||
// Override the URL by pointing the client at the test server.
|
||||
// We can't easily override the URL without a custom transport, so use a
|
||||
// round-trip wrapper instead.
|
||||
c.httpClient.Transport = rewriteTransport{base: http.DefaultTransport, target: srv.URL}
|
||||
|
||||
if err := c.CreateRelease(context.Background(), "v1.0.0", "release notes"); err != nil {
|
||||
t.Fatalf("CreateRelease: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateReleaseAPIError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
io.WriteString(w, `{"message":"Validation Failed"}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New("tok", "owner/repo")
|
||||
c.httpClient = &http.Client{Transport: rewriteTransport{base: http.DefaultTransport, target: srv.URL}}
|
||||
|
||||
err := c.CreateRelease(context.Background(), "v1.0.0", "body")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 422 response")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "422") {
|
||||
t.Errorf("error should mention status code: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateReleaseAPIErrorNoMessage(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
io.WriteString(w, `not json`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New("tok", "owner/repo")
|
||||
c.httpClient = &http.Client{Transport: rewriteTransport{base: http.DefaultTransport, target: srv.URL}}
|
||||
|
||||
err := c.CreateRelease(context.Background(), "v1.0.0", "body")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 500 response")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "500") {
|
||||
t.Errorf("error should mention status code: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateReleaseRequestFails(t *testing.T) {
|
||||
c := New("tok", "owner/repo")
|
||||
// Use a transport that always fails.
|
||||
c.httpClient = &http.Client{Transport: alwaysFailTransport{}}
|
||||
|
||||
err := c.CreateRelease(context.Background(), "v1.0.0", "body")
|
||||
if err == nil {
|
||||
t.Fatal("expected error when HTTP request fails")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateReleaseBadURL(t *testing.T) {
|
||||
// A repo containing a null byte makes the URL unparseable by http.NewRequestWithContext.
|
||||
c := New("tok", "owner/repo\x00bad")
|
||||
|
||||
err := c.CreateRelease(context.Background(), "v1.0.0", "body")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid URL")
|
||||
}
|
||||
}
|
||||
|
||||
// rewriteTransport redirects all requests to a test server URL.
|
||||
type rewriteTransport struct {
|
||||
base http.RoundTripper
|
||||
target string
|
||||
}
|
||||
|
||||
func (rt rewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req2 := req.Clone(req.Context())
|
||||
req2.URL.Scheme = "http"
|
||||
req2.URL.Host = strings.TrimPrefix(rt.target, "http://")
|
||||
return rt.base.RoundTrip(req2)
|
||||
}
|
||||
|
||||
// alwaysFailTransport returns an error for every request.
|
||||
type alwaysFailTransport struct{}
|
||||
|
||||
func (alwaysFailTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
||||
return nil, &testTransportError{"connection refused"}
|
||||
}
|
||||
|
||||
type testTransportError struct{ msg string }
|
||||
|
||||
func (e *testTransportError) Error() string { return e.msg }
|
||||
Reference in New Issue
Block a user