a73x

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)
	}
}