166 lines
5.4 KiB
Go
166 lines
5.4 KiB
Go
package controllers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"nearle/services"
|
|
"nearle/services/tools"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
)
|
|
|
|
/*
|
|
A server that can say what it is.
|
|
|
|
This exists because of a day spent unable to answer two questions about a
|
|
running deployment: which build is it, and does the assistant have a model. Both
|
|
were knowable inside the container and neither was reachable from outside —
|
|
`/assistant/status` sits behind the session guard, and a 401 from `/v1/web`
|
|
proves nothing, because the middleware answers before routing and a route that
|
|
does not exist returns the same 401 as one that does.
|
|
|
|
So the tests that matter here are about what it answers WITHOUT a session, and
|
|
about what it refuses to include.
|
|
*/
|
|
|
|
func healthApp(t *testing.T, assistantReady bool, hasDatabase bool) *fiber.App {
|
|
t.Helper()
|
|
|
|
app := fiber.New()
|
|
controller := NewHealthController(stubAssistant{ready: assistantReady}, hasDatabase)
|
|
app.Get("/live/api/v1/health", controller.Health)
|
|
return app
|
|
}
|
|
|
|
func readHealth(t *testing.T, app *fiber.App) (int, map[string]any, string) {
|
|
t.Helper()
|
|
|
|
resp, err := app.Test(httptest.NewRequest("GET", "/live/api/v1/health", nil), -1)
|
|
if err != nil {
|
|
t.Fatalf("health: %v", err)
|
|
}
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
|
|
var envelope struct {
|
|
Details map[string]any `json:"details"`
|
|
}
|
|
if err := json.Unmarshal(raw, &envelope); err != nil {
|
|
t.Fatalf("not the envelope the console unwraps: %s", raw)
|
|
}
|
|
return resp.StatusCode, envelope.Details, string(raw)
|
|
}
|
|
|
|
func TestHealthAnswersWithoutASession(t *testing.T) {
|
|
// The point. A health check that needs a credential cannot be used by the
|
|
// person working out why credentials are not working — which is exactly
|
|
// when somebody reaches for it.
|
|
status, details, body := readHealth(t, healthApp(t, true, true))
|
|
|
|
if status != fiber.StatusOK {
|
|
t.Fatalf("HTTP %d without a session: %s", status, body)
|
|
}
|
|
if details["version"] == nil {
|
|
t.Fatalf("no build id: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestHealthSaysWhetherTheAssistantHasAModel(t *testing.T) {
|
|
// "I set the key and redeployed — did it take?" took a day to answer. This
|
|
// is that answer, in one unauthenticated request.
|
|
_, ready, _ := readHealth(t, healthApp(t, true, true))
|
|
if ready["assistant"] != true {
|
|
t.Fatalf("a configured assistant reported as %v", ready["assistant"])
|
|
}
|
|
|
|
_, off, body := readHealth(t, healthApp(t, false, true))
|
|
if off["assistant"] != false {
|
|
t.Fatalf("an unconfigured assistant reported as %v: %s", off["assistant"], body)
|
|
}
|
|
}
|
|
|
|
func TestHealthNeverLeaksTheConfiguration(t *testing.T) {
|
|
// Booleans, never values. WHICH model, at which endpoint, under which key is
|
|
// not operational information, and the `reason` string on /assistant/status
|
|
// names environment variables — that stays behind the guard.
|
|
_, _, body := readHealth(t, healthApp(t, false, true))
|
|
|
|
for _, secret := range []string{
|
|
"ASSISTANT_", "api.groq.com", "gsk_", "openai/gpt-oss", "POS_TOKEN", "password",
|
|
} {
|
|
if strings.Contains(strings.ToLower(body), strings.ToLower(secret)) {
|
|
t.Fatalf("%q is exposed on an unauthenticated endpoint: %s", secret, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHealthSurvivesAServerWithNothingWiredUp(t *testing.T) {
|
|
// A deployment with no database and no model must still ANSWER. This is the
|
|
// state in which somebody is most likely to ask, and a 500 here would leave
|
|
// them exactly where they started.
|
|
app := fiber.New()
|
|
app.Get("/live/api/v1/health", NewHealthController(nil, false).Health)
|
|
|
|
resp, err := app.Test(httptest.NewRequest("GET", "/live/api/v1/health", nil), -1)
|
|
if err != nil {
|
|
t.Fatalf("health: %v", err)
|
|
}
|
|
if resp.StatusCode != fiber.StatusOK {
|
|
t.Fatalf("a bare server could not report its own health: HTTP %d", resp.StatusCode)
|
|
}
|
|
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
var envelope struct {
|
|
Details map[string]any `json:"details"`
|
|
}
|
|
_ = json.Unmarshal(raw, &envelope)
|
|
|
|
if envelope.Details["assistant"] != false || envelope.Details["database"] != false {
|
|
t.Fatalf("a bare server claimed to be wired up: %s", raw)
|
|
}
|
|
}
|
|
|
|
func TestAnUnstampedBuildSaysSoRatherThanGuessing(t *testing.T) {
|
|
// "unknown" is informative: it means nothing stamped the image, so the
|
|
// version cannot be trusted to date it. Inventing one would be worse than
|
|
// admitting it.
|
|
original := Version
|
|
Version = "unknown"
|
|
defer func() { Version = original }()
|
|
|
|
got := buildVersion()
|
|
// Either the toolchain recorded a revision, or it says unknown. What it must
|
|
// not do is return an empty string, which renders as a blank field and reads
|
|
// like the endpoint is broken.
|
|
if strings.TrimSpace(got) == "" {
|
|
t.Fatal("the build id is blank")
|
|
}
|
|
}
|
|
|
|
func TestAStampedBuildIsReported(t *testing.T) {
|
|
original := Version
|
|
Version = "abc1234"
|
|
defer func() { Version = original }()
|
|
|
|
_, details, body := readHealth(t, healthApp(t, true, true))
|
|
if details["version"] != "abc1234" {
|
|
t.Fatalf("the stamped build id was not reported: %s", body)
|
|
}
|
|
}
|
|
|
|
// stubAssistant is only ever asked one question.
|
|
type stubAssistant struct{ ready bool }
|
|
|
|
func (s stubAssistant) Available() bool { return s.ready }
|
|
func (s stubAssistant) Unavailable() string { return "" }
|
|
func (s stubAssistant) Ask(_ context.Context, _, _ string, _ tools.Caller) (services.AssistantAnswer, error) {
|
|
return services.AssistantAnswer{}, nil
|
|
}
|
|
func (s stubAssistant) Approve(_ context.Context, _, _ string, _ tools.Caller) (services.AssistantAnswer, error) {
|
|
return services.AssistantAnswer{}, nil
|
|
}
|