test(longhorn): cover client request and backup flows
This commit is contained in:
parent
798bc510bd
commit
d975d9076f
571
internal/longhorn/client_test.go
Normal file
571
internal/longhorn/client_test.go
Normal file
@ -0,0 +1,571 @@
|
||||
package longhorn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewTrimsBaseURLAndSetsDefaultTimeout(t *testing.T) {
|
||||
client := New("http://longhorn.example.test/")
|
||||
|
||||
if client.baseURL != "http://longhorn.example.test" {
|
||||
t.Fatalf("expected trimmed base URL, got %q", client.baseURL)
|
||||
}
|
||||
if client.http == nil {
|
||||
t.Fatalf("expected HTTP client to be initialized")
|
||||
}
|
||||
if client.http.Timeout != defaultTimeout {
|
||||
t.Fatalf("expected timeout %s, got %s", defaultTimeout, client.http.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIErrorErrorFormatsStatusAndMessage(t *testing.T) {
|
||||
err := (&APIError{Status: http.StatusBadGateway, Message: "proxy exploded"}).Error()
|
||||
if err != "longhorn api error: status=502 message=proxy exploded" {
|
||||
t.Fatalf("unexpected API error string: %q", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreVolumeOperationsUseExpectedRequests(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
type requestRecord struct {
|
||||
method string
|
||||
path string
|
||||
query string
|
||||
body string
|
||||
}
|
||||
|
||||
var requests []requestRecord
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
requests = append(requests, requestRecord{
|
||||
method: r.Method,
|
||||
path: r.URL.Path,
|
||||
query: r.URL.RawQuery,
|
||||
body: string(body),
|
||||
})
|
||||
|
||||
switch {
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v1/volumes/pv/name" && r.URL.RawQuery == "action=snapshotCreate":
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v1/volumes/pv/name" && r.URL.RawQuery == "action=snapshotBackup":
|
||||
_, _ = w.Write([]byte(`{"name":"pv/name","size":"1073741824","numberOfReplicas":2}`))
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/v1/volumes/pv/name":
|
||||
_, _ = w.Write([]byte(`{"name":"pv/name","size":"2048","numberOfReplicas":3}`))
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v1/volumes" && r.URL.RawQuery == "":
|
||||
_, _ = w.Write([]byte(`{"name":"restored","size":"4096","numberOfReplicas":2}`))
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v1/volumes/pv/name" && r.URL.RawQuery == "action=pvcCreate":
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
case r.Method == http.MethodDelete && r.URL.Path == "/v1/volumes/pv/name":
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s?%s body=%s", r.Method, r.URL.Path, r.URL.RawQuery, string(body))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New(server.URL)
|
||||
|
||||
if err := client.CreateSnapshot(ctx, "pv/name", "snap-1", map[string]string{"env": "dev"}); err != nil {
|
||||
t.Fatalf("CreateSnapshot: %v", err)
|
||||
}
|
||||
|
||||
backupVolume, err := client.SnapshotBackup(ctx, "pv/name", "snap-1", map[string]string{"env": "dev"}, "incremental")
|
||||
if err != nil {
|
||||
t.Fatalf("SnapshotBackup: %v", err)
|
||||
}
|
||||
if backupVolume.Name != "pv/name" || backupVolume.NumberOfReplicas != 2 {
|
||||
t.Fatalf("unexpected snapshot backup response: %#v", backupVolume)
|
||||
}
|
||||
|
||||
volume, err := client.GetVolume(ctx, "pv/name")
|
||||
if err != nil {
|
||||
t.Fatalf("GetVolume: %v", err)
|
||||
}
|
||||
if volume.Name != "pv/name" || volume.NumberOfReplicas != 3 {
|
||||
t.Fatalf("unexpected volume response: %#v", volume)
|
||||
}
|
||||
|
||||
restored, err := client.CreateVolumeFromBackup(ctx, "restored", "4096", 2, "s3://bucket/backup")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateVolumeFromBackup: %v", err)
|
||||
}
|
||||
if restored.Name != "restored" || restored.NumberOfReplicas != 2 {
|
||||
t.Fatalf("unexpected restored volume response: %#v", restored)
|
||||
}
|
||||
|
||||
if err := client.CreatePVC(ctx, "pv/name", "apps", "data"); err != nil {
|
||||
t.Fatalf("CreatePVC: %v", err)
|
||||
}
|
||||
if err := client.DeleteVolume(ctx, "pv/name"); err != nil {
|
||||
t.Fatalf("DeleteVolume: %v", err)
|
||||
}
|
||||
|
||||
if len(requests) != 6 {
|
||||
t.Fatalf("expected 6 requests, got %#v", requests)
|
||||
}
|
||||
|
||||
assertBodyContains(t, requests[0].body, `"name":"snap-1"`)
|
||||
assertBodyContains(t, requests[0].body, `"labels":{"env":"dev"}`)
|
||||
assertBodyContains(t, requests[1].body, `"backupMode":"incremental"`)
|
||||
assertBodyContains(t, requests[3].body, `"fromBackup":"s3://bucket/backup"`)
|
||||
assertBodyContains(t, requests[3].body, `"numberOfReplicas":2`)
|
||||
assertBodyContains(t, requests[4].body, `"namespace":"apps"`)
|
||||
assertBodyContains(t, requests[4].body, `"pvcName":"data"`)
|
||||
}
|
||||
|
||||
func TestCreateVolumeFromBackupOmitsReplicaCountWhenNotRequested(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var body string
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/v1/volumes" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
raw, _ := io.ReadAll(r.Body)
|
||||
body = string(raw)
|
||||
_, _ = w.Write([]byte(`{"name":"restored","size":"4096"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New(server.URL)
|
||||
if _, err := client.CreateVolumeFromBackup(ctx, "restored", "4096", 0, "s3://bucket/backup"); err != nil {
|
||||
t.Fatalf("CreateVolumeFromBackup: %v", err)
|
||||
}
|
||||
|
||||
if strings.Contains(body, "numberOfReplicas") {
|
||||
t.Fatalf("expected zero-replica request to omit numberOfReplicas, body=%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolumeOperationsPropagateAPIErrorsAndOptionalBranches(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("snapshot backup without backup mode", func(t *testing.T) {
|
||||
var body string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/v1/volumes/pv/name" || r.URL.RawQuery != "action=snapshotBackup" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
raw, _ := io.ReadAll(r.Body)
|
||||
body = string(raw)
|
||||
_, _ = w.Write([]byte(`{"name":"pv/name","size":"1024"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New(server.URL)
|
||||
if _, err := client.SnapshotBackup(ctx, "pv/name", "snap-plain", map[string]string{"team": "atlas"}, ""); err != nil {
|
||||
t.Fatalf("SnapshotBackup without backup mode: %v", err)
|
||||
}
|
||||
if strings.Contains(body, "backupMode") {
|
||||
t.Fatalf("expected empty backup mode to be omitted, body=%s", body)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get volume error", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "volume missing", http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New(server.URL)
|
||||
_, err := client.GetVolume(ctx, "pv/name")
|
||||
apiErr, ok := err.(*APIError)
|
||||
if !ok || apiErr.Status != http.StatusNotFound {
|
||||
t.Fatalf("expected APIError 404 for GetVolume, got %#v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("create volume from backup error", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "create failed", http.StatusBadGateway)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New(server.URL)
|
||||
_, err := client.CreateVolumeFromBackup(ctx, "restored", "4096", 1, "s3://bucket/backup")
|
||||
apiErr, ok := err.(*APIError)
|
||||
if !ok || apiErr.Status != http.StatusBadGateway {
|
||||
t.Fatalf("expected APIError 502 for CreateVolumeFromBackup, got %#v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetBackupVolumeUsesDirectAndFallbackLookup(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("direct hit", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || r.URL.Path != "/v1/backupvolumes/vol-a" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"name":"vol-a","actions":{"backupList":"http://example.test/list"}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New(server.URL)
|
||||
volume, err := client.GetBackupVolume(ctx, "vol-a")
|
||||
if err != nil {
|
||||
t.Fatalf("GetBackupVolume direct hit: %v", err)
|
||||
}
|
||||
if volume.Name != "vol-a" || volume.Actions["backupList"] != "http://example.test/list" {
|
||||
t.Fatalf("unexpected backup volume: %#v", volume)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fallback list", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/v1/backupvolumes/vol-b":
|
||||
http.Error(w, "missing", http.StatusNotFound)
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/v1/backupvolumes":
|
||||
_, _ = w.Write([]byte(`{"data":[{"name":"vol-b","actions":{"backupList":"http://example.test/list-b"}}]}`))
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New(server.URL)
|
||||
volume, err := client.GetBackupVolume(ctx, "vol-b")
|
||||
if err != nil {
|
||||
t.Fatalf("GetBackupVolume fallback: %v", err)
|
||||
}
|
||||
if volume.Name != "vol-b" {
|
||||
t.Fatalf("expected fallback lookup to find vol-b, got %#v", volume)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fallback missing item", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/v1/backupvolumes/vol-c":
|
||||
http.Error(w, "missing", http.StatusNotFound)
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/v1/backupvolumes":
|
||||
_, _ = w.Write([]byte(`{"data":[{"name":"other","actions":{"backupList":"http://example.test/list"}}]}`))
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New(server.URL)
|
||||
_, err := client.GetBackupVolume(ctx, "vol-c")
|
||||
if err == nil || !strings.Contains(err.Error(), "not found") {
|
||||
t.Fatalf("expected missing fallback item error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-404 direct error", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "controller unhappy", http.StatusBadGateway)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New(server.URL)
|
||||
_, err := client.GetBackupVolume(ctx, "vol-d")
|
||||
apiErr, ok := err.(*APIError)
|
||||
if !ok || apiErr.Status != http.StatusBadGateway {
|
||||
t.Fatalf("expected direct APIError 502, got %#v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fallback list error", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/v1/backupvolumes/vol-e":
|
||||
http.Error(w, "missing", http.StatusNotFound)
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/v1/backupvolumes":
|
||||
http.Error(w, "list failed", http.StatusBadGateway)
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New(server.URL)
|
||||
_, err := client.GetBackupVolume(ctx, "vol-e")
|
||||
apiErr, ok := err.(*APIError)
|
||||
if !ok || apiErr.Status != http.StatusBadGateway {
|
||||
t.Fatalf("expected fallback list APIError 502, got %#v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestListBackupsAndFindBackupSelections(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
serverURL := ""
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/v1/backupvolumes/vol-a":
|
||||
_, _ = w.Write([]byte(`{"name":"vol-a","actions":{"backupList":"` + serverURL + `/backups"}}`))
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/backups":
|
||||
_, _ = w.Write([]byte(`{"data":[
|
||||
{"name":"backup-old","snapshotName":"snap-old","volumeName":"vol-a","created":"2026-04-19T10:00:00Z","state":"Completed","url":"s3://bucket/old","size":"1Gi"},
|
||||
{"name":"backup-new","snapshotName":"snap-new","volumeName":"vol-a","created":"2026-04-20T10:00:00Z","state":"Completed","url":"s3://bucket/new","size":"2Gi"},
|
||||
{"name":"backup-invalid","snapshotName":"snap-invalid","volumeName":"vol-a","created":"not-a-time","state":"Completed","url":"s3://bucket/invalid","size":"3Gi"},
|
||||
{"name":"backup-failed","snapshotName":"snap-failed","volumeName":"vol-a","created":"2026-04-21T10:00:00Z","state":"Failed","url":"s3://bucket/failed","size":"4Gi"}
|
||||
]}`))
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
serverURL = server.URL
|
||||
|
||||
client := New(server.URL)
|
||||
|
||||
backups, err := client.ListBackups(ctx, "vol-a")
|
||||
if err != nil {
|
||||
t.Fatalf("ListBackups: %v", err)
|
||||
}
|
||||
if len(backups) != 4 {
|
||||
t.Fatalf("expected four backups, got %#v", backups)
|
||||
}
|
||||
|
||||
latest, err := client.FindBackup(ctx, "vol-a", "latest")
|
||||
if err != nil {
|
||||
t.Fatalf("FindBackup latest: %v", err)
|
||||
}
|
||||
if latest.Name != "backup-new" {
|
||||
t.Fatalf("expected latest completed backup-new, got %#v", latest)
|
||||
}
|
||||
|
||||
bySnapshot, err := client.FindBackup(ctx, "vol-a", "snap-old")
|
||||
if err != nil {
|
||||
t.Fatalf("FindBackup by snapshot: %v", err)
|
||||
}
|
||||
if bySnapshot.Name != "backup-old" {
|
||||
t.Fatalf("expected snapshot lookup to return backup-old, got %#v", bySnapshot)
|
||||
}
|
||||
|
||||
byURL, err := client.FindBackup(ctx, "vol-a", "s3://bucket/invalid")
|
||||
if err != nil {
|
||||
t.Fatalf("FindBackup by URL: %v", err)
|
||||
}
|
||||
if byURL.Name != "backup-invalid" {
|
||||
t.Fatalf("expected URL lookup to return backup-invalid, got %#v", byURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListBackupsAndFindBackupErrors(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("missing backupList action", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"name":"vol-a","actions":{}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New(server.URL)
|
||||
_, err := client.ListBackups(ctx, "vol-a")
|
||||
if err == nil || !strings.Contains(err.Error(), "backup list action missing") {
|
||||
t.Fatalf("expected missing backupList action error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("backup list request fails", func(t *testing.T) {
|
||||
serverURL := ""
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/v1/backupvolumes/vol-a":
|
||||
_, _ = w.Write([]byte(`{"name":"vol-a","actions":{"backupList":"` + serverURL + `/backups"}}`))
|
||||
case "/backups":
|
||||
http.Error(w, "list failed", http.StatusBadGateway)
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
serverURL = server.URL
|
||||
|
||||
client := New(server.URL)
|
||||
_, err := client.ListBackups(ctx, "vol-a")
|
||||
apiErr, ok := err.(*APIError)
|
||||
if !ok || apiErr.Status != http.StatusBadGateway {
|
||||
t.Fatalf("expected backup list APIError 502, got %#v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no backups found", func(t *testing.T) {
|
||||
serverURL := ""
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/v1/backupvolumes/vol-a":
|
||||
_, _ = w.Write([]byte(`{"name":"vol-a","actions":{"backupList":"` + serverURL + `/backups"}}`))
|
||||
case "/backups":
|
||||
_, _ = w.Write([]byte(`{"data":[]}`))
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
serverURL = server.URL
|
||||
|
||||
client := New(server.URL)
|
||||
_, err := client.FindBackup(ctx, "vol-a", "latest")
|
||||
if err == nil || !strings.Contains(err.Error(), "no backups found") {
|
||||
t.Fatalf("expected no backups found error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("named backup not found", func(t *testing.T) {
|
||||
serverURL := ""
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/v1/backupvolumes/vol-a":
|
||||
_, _ = w.Write([]byte(`{"name":"vol-a","actions":{"backupList":"` + serverURL + `/backups"}}`))
|
||||
case "/backups":
|
||||
_, _ = w.Write([]byte(`{"data":[{"name":"backup-a","snapshotName":"snap-a","volumeName":"vol-a","created":"2026-04-20T10:00:00Z","state":"Completed","url":"s3://bucket/a","size":"1Gi"}]}`))
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
serverURL = server.URL
|
||||
|
||||
client := New(server.URL)
|
||||
_, err := client.FindBackup(ctx, "vol-a", "missing")
|
||||
if err == nil || !strings.Contains(err.Error(), "backup missing not found") {
|
||||
t.Fatalf("expected named backup missing error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no completed backups", func(t *testing.T) {
|
||||
serverURL := ""
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/v1/backupvolumes/vol-a":
|
||||
_, _ = w.Write([]byte(`{"name":"vol-a","actions":{"backupList":"` + serverURL + `/backups"}}`))
|
||||
case "/backups":
|
||||
_, _ = w.Write([]byte(`{"data":[{"name":"backup-a","snapshotName":"snap-a","volumeName":"vol-a","created":"2026-04-20T10:00:00Z","state":"Failed","url":"s3://bucket/a","size":"1Gi"}]}`))
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
serverURL = server.URL
|
||||
|
||||
client := New(server.URL)
|
||||
_, err := client.FindBackup(ctx, "vol-a", "latest")
|
||||
if err == nil || !strings.Contains(err.Error(), "no completed backups found") {
|
||||
t.Fatalf("expected no completed backups error, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDoJSONErrorPaths(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("encode request", func(t *testing.T) {
|
||||
client := New("http://example.test")
|
||||
err := client.doJSON(ctx, http.MethodPost, "http://example.test/path", map[string]any{"bad": func() {}}, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "encode request") {
|
||||
t.Fatalf("expected encode request error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("build request", func(t *testing.T) {
|
||||
client := New("http://example.test")
|
||||
err := client.doJSON(ctx, http.MethodGet, "://bad-url", nil, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "build request") {
|
||||
t.Fatalf("expected build request error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("request failed", func(t *testing.T) {
|
||||
client := New("http://example.test")
|
||||
client.http = &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return nil, errors.New("dial failed")
|
||||
})}
|
||||
err := client.doJSON(ctx, http.MethodGet, "http://example.test/path", nil, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "request failed") {
|
||||
t.Fatalf("expected request failed error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("read response", func(t *testing.T) {
|
||||
client := New("http://example.test")
|
||||
client.http = &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(errReader{}),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
})}
|
||||
err := client.doJSON(ctx, http.MethodGet, "http://example.test/path", nil, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "read response") {
|
||||
t.Fatalf("expected read response error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("api error", func(t *testing.T) {
|
||||
client := New("http://example.test")
|
||||
client.http = &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusBadGateway,
|
||||
Body: io.NopCloser(strings.NewReader("proxy unhappy")),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
})}
|
||||
err := client.doJSON(ctx, http.MethodGet, "http://example.test/path", nil, nil)
|
||||
apiErr, ok := err.(*APIError)
|
||||
if !ok || apiErr.Status != http.StatusBadGateway || apiErr.Message != "proxy unhappy" {
|
||||
t.Fatalf("expected APIError 502/proxy unhappy, got %#v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("decode response", func(t *testing.T) {
|
||||
client := New("http://example.test")
|
||||
client.http = &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader("{not-json")),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
})}
|
||||
var out Volume
|
||||
err := client.doJSON(ctx, http.MethodGet, "http://example.test/path", nil, &out)
|
||||
if err == nil || !strings.Contains(err.Error(), "decode response") {
|
||||
t.Fatalf("expected decode response error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success with empty body", func(t *testing.T) {
|
||||
client := New("http://example.test")
|
||||
client.http = &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusNoContent,
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
})}
|
||||
if err := client.doJSON(ctx, http.MethodDelete, "http://example.test/path", nil, nil); err != nil {
|
||||
t.Fatalf("expected empty-body success, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func assertBodyContains(t *testing.T, body, want string) {
|
||||
t.Helper()
|
||||
if !strings.Contains(body, want) {
|
||||
t.Fatalf("expected body %q to contain %q", body, want)
|
||||
}
|
||||
}
|
||||
|
||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return fn(req)
|
||||
}
|
||||
|
||||
type errReader struct{}
|
||||
|
||||
func (errReader) Read([]byte) (int, error) {
|
||||
return 0, errors.New("boom")
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user