Files
go-sip/internal/configread/client_test.go
T

178 lines
5.8 KiB
Go

package configread
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
const (
mockDispatcherID = "c046b893-8628-4589-ae50-619d049248a6"
mockTaskID = "task-mock"
mockTenantID = "tenant-id-mock"
mockTenantKey = "tenant-mock"
mockTestSecret = "test-secret"
)
func TestClientReadTaskRequestsAndValidatesThreeConfigEndpoints(t *testing.T) {
fixtures := validConfigFixtures(t)
var got []string
mux := http.NewServeMux()
for path, body := range fixtures {
path, body := path, body
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
got = append(got, r.Method+" "+r.URL.RequestURI())
if r.Method != http.MethodGet {
t.Errorf("method = %q, want GET", r.Method)
}
if r.Header.Get(dispatcherIDHeader) != mockDispatcherID {
t.Errorf("dispatcher ID header = %q", r.Header.Get(dispatcherIDHeader))
}
if r.Header.Get(dispatcherSecretHeader) != mockTestSecret {
t.Errorf("dispatcher secret header = %q", r.Header.Get(dispatcherSecretHeader))
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(body)
})
}
server := httptest.NewServer(mux)
defer server.Close()
client := newMockClient(t, server)
snapshot, err := client.ReadTask(context.Background(), mockTaskID, mockTenantID)
if err != nil {
t.Fatal(err)
}
if len(snapshot.SIP) == 0 || len(snapshot.Task) == 0 || len(snapshot.TenantQuota) == 0 {
t.Fatalf("incomplete config snapshot: %+v", snapshot)
}
if snapshot.TaskID != mockTaskID || snapshot.TenantID != mockTenantID || snapshot.TenantKey != mockTenantKey {
t.Fatalf("snapshot identity = %q/%q/%q", snapshot.TaskID, snapshot.TenantID, snapshot.TenantKey)
}
want := []string{
"GET /internal/v1/dispatcher/sip",
"GET /internal/v1/dispatcher/task/" + mockTaskID,
"GET /internal/v1/dispatcher/tenant/" + mockTenantID + "/quota",
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("requests = %#v, want %#v", got, want)
}
}
func TestClientReadTaskRejectsSchemaInvalidResponse(t *testing.T) {
fixtures := validConfigFixtures(t)
fixtures[configReadPath+"/sip"] = readConfigFixture(t, "config-read-invalid-extra-property-v0.1.json")
server := configFixtureServer(t, fixtures)
defer server.Close()
if _, err := newMockClient(t, server).ReadTask(context.Background(), mockTaskID, mockTenantID); err == nil {
t.Fatal("ReadTask accepted a response rejected by the frozen Schema")
}
}
func TestClientReadTaskRejectsIdentityMismatch(t *testing.T) {
cases := []struct {
name string
path string
old string
new string
}{
{"task id", configReadPath + "/task/" + mockTaskID, `"task_id": "task-mock"`, `"task_id": "task-other"`},
{"quota tenant key", configReadPath + "/tenant/" + mockTenantID + "/quota", `"tenant_key": "tenant-mock"`, `"tenant_key": "tenant-other"`},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
fixtures := validConfigFixtures(t)
fixtures[tc.path] = replaceFixtureText(t, fixtures[tc.path], tc.old, tc.new)
server := configFixtureServer(t, fixtures)
defer server.Close()
if _, err := newMockClient(t, server).ReadTask(context.Background(), mockTaskID, mockTenantID); err == nil {
t.Fatal("ReadTask accepted an identity mismatch")
}
})
}
}
func TestClientReadTaskReturnsStructuredHTTPFailure(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
_, _ = fmt.Fprint(w, `{"schema_version":"config-read.v0.1","resource":"error","error":{"code":"service_unavailable","message":"temporarily unavailable"}}`)
}))
defer server.Close()
_, err := newMockClient(t, server).ReadTask(context.Background(), mockTaskID, mockTenantID)
var statusErr *HTTPError
if !errors.As(err, &statusErr) {
t.Fatalf("error = %v, want *HTTPError", err)
}
if statusErr.StatusCode != http.StatusServiceUnavailable || statusErr.Code != "service_unavailable" {
t.Fatalf("HTTP error = %+v", statusErr)
}
}
func marshalDiscoveryResponse(t *testing.T, response map[string]any) []byte {
t.Helper()
body, err := json.Marshal(response)
if err != nil {
t.Fatal(err)
}
return body
}
func newMockClient(t *testing.T, server *httptest.Server) *Client {
t.Helper()
client, err := NewClient(server.URL, mockDispatcherID, mockTestSecret, server.Client())
if err != nil {
t.Fatal(err)
}
return client
}
func validConfigFixtures(t *testing.T) map[string][]byte {
t.Helper()
return map[string][]byte{
configReadPath + "/sip": readConfigFixture(t, "config-read-sip-v0.1.json"),
configReadPath + "/tasks": readConfigFixture(t, "task-discovery-snapshot-v0.2.json"),
configReadPath + "/task/" + mockTaskID: readConfigFixture(t, "config-read-task-v0.1.json"),
configReadPath + "/tenant/" + mockTenantID + "/quota": readConfigFixture(t, "config-read-tenant-quota-v0.1.json"),
}
}
func configFixtureServer(t *testing.T, fixtures map[string][]byte) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, ok := fixtures[r.URL.Path]
if !ok {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(body)
}))
}
func readConfigFixture(t *testing.T, name string) []byte {
t.Helper()
body, err := os.ReadFile(filepath.Join("..", "..", "docs", "contracts", "examples", name))
if err != nil {
t.Fatal(err)
}
return body
}
func replaceFixtureText(t *testing.T, body []byte, old, replacement string) []byte {
t.Helper()
if !strings.Contains(string(body), old) {
t.Fatalf("fixture does not contain %q", old)
}
return []byte(strings.Replace(string(body), old, replacement, 1))
}