Files
ReleaseKit/internal/gitea/release_test.go

342 lines
11 KiB
Go

package gitea
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
const testCommit = "0123456789012345678901234567890123456789"
const testNote = "# Example v1.2.3\n\nRelease details.\n"
const testToken = "fixture-secret-token"
func TestEnsureReleaseFindsMatchingReleaseAndPreservesTitle(t *testing.T) {
server, _ := newReleaseServer(t, releaseFixture{title: "Historical title"}, false)
result, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if err != nil {
t.Fatal(err)
}
if result.Created || result.Release.Title != "Historical title" {
t.Fatalf("result = %#v", result)
}
}
func TestEnsureReleaseCreatesMissingPageFromCommittedNote(t *testing.T) {
server, _ := newReleaseServer(t, releaseFixture{}, true)
result, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if err != nil {
t.Fatal(err)
}
if !result.Created || result.Release.Title != "Example v1.2.3" || result.Release.Body != testNote {
t.Fatalf("result = %#v", result)
}
}
func TestEnsureReleaseConflictDoesNotEditExistingPage(t *testing.T) {
server, state := newReleaseServer(t, releaseFixture{title: "Keep this title", body: "Different body"}, false)
_, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if failureKind(t, err) != FailureConflict {
t.Fatalf("error = %v", err)
}
if got := state.posts; got != 0 {
t.Fatalf("release creates = %d, want 0", got)
}
}
func TestEnsureReleaseUnauthorizedIsStructuredAndHidesToken(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, http.MethodGet, "/api/v1/repos/owner/project/tags/v1.2.3")
w.WriteHeader(http.StatusUnauthorized)
}))
defer server.Close()
_, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if failureKind(t, err) != FailureUnauthorized {
t.Fatalf("error = %v", err)
}
if fmt.Sprint(err) == "" || contains(fmt.Sprint(err), testToken) {
t.Fatalf("error exposes unexpected content: %v", err)
}
}
func TestEnsureReleaseMalformedTagResponseStopsBeforeCreate(t *testing.T) {
posts := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
posts++
}
assertRequest(t, r, http.MethodGet, "/api/v1/repos/owner/project/tags/v1.2.3")
_, _ = w.Write([]byte("{"))
}))
defer server.Close()
_, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if failureKind(t, err) != FailureMalformed || posts != 0 {
t.Fatalf("error = %v, posts = %d", err, posts)
}
}
func TestEnsureReleaseWrongHostedTagStopsBeforeCreate(t *testing.T) {
posts := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, http.MethodGet, "/api/v1/repos/owner/project/tags/v1.2.3")
if r.Method == http.MethodPost {
posts++
}
_, _ = fmt.Fprintf(w, `{"commit":{"sha":"%040d"}}`, 7)
}))
defer server.Close()
_, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if failureKind(t, err) != FailureConflict || posts != 0 {
t.Fatalf("error = %v, posts = %d", err, posts)
}
}
func TestEnsureReleaseMissingHostedTagStopsBeforeCreate(t *testing.T) {
posts := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, http.MethodGet, "/api/v1/repos/owner/project/tags/v1.2.3")
if r.Method == http.MethodPost {
posts++
}
http.NotFound(w, r)
}))
defer server.Close()
_, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if failureKind(t, err) != FailureNotFound || posts != 0 {
t.Fatalf("error = %v, posts = %d", err, posts)
}
}
func TestEnsureReleaseHonorsContextCancellation(t *testing.T) {
called := false
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true }))
defer server.Close()
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := testClient(server).EnsureRelease(ctx, testRequest(server.URL))
if !errors.Is(err, context.Canceled) || called {
t.Fatalf("error = %v, server called = %t", err, called)
}
}
func TestEnsureReleaseResolvesLostCreateAcknowledgmentWithOneLookup(t *testing.T) {
created := false
lookups := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAuth(t, r)
switch r.URL.Path {
case "/api/v1/repos/owner/project/tags/v1.2.3":
assertRequest(t, r, http.MethodGet, r.URL.Path)
writeTag(t, w, testCommit)
case "/api/v1/repos/owner/project/releases/tags/v1.2.3":
assertRequest(t, r, http.MethodGet, r.URL.Path)
lookups++
if !created {
http.NotFound(w, r)
return
}
writeRelease(t, w, releaseFixture{title: "Example v1.2.3"})
case "/api/v1/repos/owner/project/releases":
assertRequest(t, r, http.MethodPost, r.URL.Path)
var payload createReleaseRequest
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
if payload.TagName != "v1.2.3" || payload.TargetCommitish != testCommit || payload.Name != "Example v1.2.3" || payload.Body != testNote || payload.Draft || payload.Prerelease {
t.Fatalf("create payload = %#v", payload)
}
created = true
hijacker, ok := w.(http.Hijacker)
if !ok {
t.Fatal("HTTP writer does not support hijacking")
}
connection, _, err := hijacker.Hijack()
if err != nil {
t.Fatal(err)
}
_ = connection.Close()
default:
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
}
}))
defer server.Close()
result, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if err != nil {
t.Fatal(err)
}
if result.Created || lookups != 2 {
t.Fatalf("result = %#v, release lookups = %d", result, lookups)
}
}
func TestEnsureReleaseResolvesCreateConflictWithOneLookup(t *testing.T) {
lookups := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAuth(t, r)
switch r.URL.Path {
case "/api/v1/repos/owner/project/tags/v1.2.3":
assertRequest(t, r, http.MethodGet, r.URL.Path)
writeTag(t, w, testCommit)
case "/api/v1/repos/owner/project/releases/tags/v1.2.3":
assertRequest(t, r, http.MethodGet, r.URL.Path)
lookups++
if lookups == 1 {
http.NotFound(w, r)
return
}
writeRelease(t, w, releaseFixture{title: "Created by another request"})
case "/api/v1/repos/owner/project/releases":
assertRequest(t, r, http.MethodPost, r.URL.Path)
w.WriteHeader(http.StatusConflict)
default:
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
}
}))
defer server.Close()
result, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if err != nil {
t.Fatal(err)
}
if result.Created || result.Release.Title != "Created by another request" || lookups != 2 {
t.Fatalf("result = %#v, release lookups = %d", result, lookups)
}
}
func TestEnsureReleaseReportsUnknownWhenAmbiguousCreateCannotBeInspected(t *testing.T) {
lookups := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAuth(t, r)
switch r.URL.Path {
case "/api/v1/repos/owner/project/tags/v1.2.3":
assertRequest(t, r, http.MethodGet, r.URL.Path)
writeTag(t, w, testCommit)
case "/api/v1/repos/owner/project/releases/tags/v1.2.3":
assertRequest(t, r, http.MethodGet, r.URL.Path)
lookups++
if lookups == 1 {
http.NotFound(w, r)
return
}
http.Error(w, "temporarily unavailable", http.StatusServiceUnavailable)
case "/api/v1/repos/owner/project/releases":
assertRequest(t, r, http.MethodPost, r.URL.Path)
w.WriteHeader(http.StatusInternalServerError)
default:
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
}
}))
defer server.Close()
_, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if failureKind(t, err) != FailureUnknown || lookups != 2 {
t.Fatalf("error = %v, release lookups = %d", err, lookups)
}
}
type releaseFixture struct {
title string
body string
draft bool
pre bool
}
type fixtureState struct{ posts int }
func newReleaseServer(t *testing.T, existing releaseFixture, createMissing bool) (*httptest.Server, *fixtureState) {
t.Helper()
state := &fixtureState{}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAuth(t, r)
switch r.URL.Path {
case "/api/v1/repos/owner/project/tags/v1.2.3":
assertRequest(t, r, http.MethodGet, r.URL.Path)
writeTag(t, w, testCommit)
case "/api/v1/repos/owner/project/releases/tags/v1.2.3":
assertRequest(t, r, http.MethodGet, r.URL.Path)
if existing.title == "" && existing.body == "" {
http.NotFound(w, r)
return
}
writeRelease(t, w, existing)
case "/api/v1/repos/owner/project/releases":
assertRequest(t, r, http.MethodPost, r.URL.Path)
state.posts++
var payload createReleaseRequest
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
if payload.TagName != "v1.2.3" || payload.TargetCommitish != testCommit || payload.Name != "Example v1.2.3" || payload.Body != testNote || payload.Draft || payload.Prerelease {
t.Fatalf("create payload = %#v", payload)
}
if !createMissing {
w.WriteHeader(http.StatusConflict)
return
}
writeRelease(t, w, releaseFixture{title: payload.Name})
default:
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
}
})
server := httptest.NewServer(handler)
return server, state
}
func writeTag(t *testing.T, w http.ResponseWriter, commit string) {
t.Helper()
w.Header().Set("Content-Type", "application/json")
_, _ = fmt.Fprintf(w, `{"name":"v1.2.3","commit":{"sha":%q}}`, commit)
}
func writeRelease(t *testing.T, w http.ResponseWriter, fixture releaseFixture) {
t.Helper()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(releaseResponse{ID: 10, HTMLURL: "https://gitea.example/releases/10", TagName: "v1.2.3", Name: fixture.title, Body: fixture.bodyOrDefault(), Draft: fixture.draft, Prerelease: fixture.pre})
}
func (fixture releaseFixture) bodyOrDefault() string {
if fixture.body == "" {
return testNote
}
return fixture.body
}
func assertRequest(t *testing.T, request *http.Request, method, path string) {
t.Helper()
if request.Method != method || request.URL.Path != path {
t.Fatalf("request = %s %s, want %s %s", request.Method, request.URL.Path, method, path)
}
assertAuth(t, request)
}
func assertAuth(t *testing.T, request *http.Request) {
t.Helper()
if got := request.Header.Get("Authorization"); got != "token "+testToken {
t.Fatalf("Authorization = %q", got)
}
}
func testClient(server *httptest.Server) Client {
return Client{HTTPClient: server.Client()}
}
func testRequest(baseURL string) Request {
return Request{Destination: Destination{BaseURL: baseURL, Owner: "owner", Repository: "project", Token: testToken}, Version: "v1.2.3", Commit: testCommit, Note: testNote}
}
func failureKind(t *testing.T, err error) FailureKind {
t.Helper()
var failure *Failure
if !errors.As(err, &failure) {
t.Fatalf("error type = %T (%v), want *Failure", err, err)
}
return failure.Kind
}
func contains(value, substring string) bool {
return len(substring) > 0 && len(value) >= len(substring) && strings.Contains(value, substring)
}