internal/agent/enrollclient/enrollclient_test.go
Ref: Size: 3.2 KiB History
package enrollclient
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"testing"
)
// startServer stands up an /api/v1/enroll handler and returns a Client aimed at
// it. handler receives the decoded request and returns the status + response
// body to send back.
func startServer(t *testing.T, handler func(Request) (int, any)) *Client {
t.Helper()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/enroll" {
t.Errorf("enroll hit wrong path %q", r.URL.Path)
}
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
t.Errorf("content-type = %q, want application/json", ct)
}
var req Request
body, _ := io.ReadAll(r.Body)
if err := json.Unmarshal(body, &req); err != nil {
t.Errorf("server could not decode request body %q: %v", body, err)
}
status, resp := handler(req)
w.WriteHeader(status)
if resp != nil {
_ = json.NewEncoder(w).Encode(resp)
}
}))
t.Cleanup(ts.Close)
return New(ts.URL)
}
func TestEnrollReturnsCredentialOn201(t *testing.T) {
var got Request
c := startServer(t, func(req Request) (int, any) {
got = req
return http.StatusCreated, map[string]string{
"host_id": "h-123",
"credential": "cred-abc",
"bridge_cidr": "10.77.1.0/24",
"server_cert_sha256": "ignored-by-agent",
}
})
resp, err := c.Enroll(context.Background(), Request{
Token: "tok", Name: "host-1", OS: "linux", Arch: "amd64", Provisioner: "cloudhv",
})
if err != nil {
t.Fatalf("Enroll returned error: %v", err)
}
// The request reached the server with every field intact.
if got != (Request{Token: "tok", Name: "host-1", OS: "linux", Arch: "amd64", Provisioner: "cloudhv"}) {
t.Errorf("server received %+v, want the posted request verbatim", got)
}
// The response decoded into the typed fields the agent consumes.
if resp.HostID != "h-123" || resp.Credential != "cred-abc" || resp.BridgeCIDR != "10.77.1.0/24" {
t.Errorf("decoded response = %+v, want host_id/credential/bridge_cidr populated", resp)
}
}
func TestEnrollMapsForbiddenToErrTokenRejected(t *testing.T) {
c := startServer(t, func(Request) (int, any) {
return http.StatusForbidden, nil
})
_, err := c.Enroll(context.Background(), Request{Token: "used"})
if !errors.Is(err, ErrTokenRejected) {
t.Fatalf("403 gave err %v, want ErrTokenRejected", err)
}
}
func TestEnrollReportsOtherStatusCodes(t *testing.T) {
c := startServer(t, func(Request) (int, any) {
return http.StatusInternalServerError, nil
})
_, err := c.Enroll(context.Background(), Request{Token: "tok"})
if err == nil {
t.Fatal("500 returned nil error")
}
if errors.Is(err, ErrTokenRejected) {
t.Errorf("500 must not map to ErrTokenRejected, got %v", err)
}
}
func TestEnrollFailsOnUnreachableServer(t *testing.T) {
// A syntactically valid but dead origin: the transport error must surface,
// not a panic or a false success.
c := New("http://127.0.0.1:1")
_, err := c.Enroll(context.Background(), Request{Token: "tok"})
if err == nil {
t.Fatal("unreachable server returned nil error")
}
if errors.Is(err, ErrTokenRejected) {
t.Errorf("transport failure must not map to ErrTokenRejected, got %v", err)
}
}