Files
felhom.eu/hub/internal/api/artifact_test.go
T
admin 39ef64e128 hub v0.16.0 + host-install v1.1.0: Day-0 artifact manifest + self-install the agent (BUNDLE slice)
Hub (v0.16.0):
- store: ArtifactManifest{agent,golden version+sha256} in hub_settings; Get/SetArtifactManifest.
- handler: GET /api/v1/artifacts/{id} (passphrase auth, mirrors config-retrieve). Unset => 200 empty.
- web: operator UI "Day-0 artifacts" card (POST /configs/artifacts), semver + 64-hex validation.
- artifact_test.go: returned-verbatim / unset-empty / 401 / 404 / store round-trip.

host-install (v1.1.0):
- new step 5/8 agent-install: manifest + git token (config-retrieve) -> fetch binary from Gitea ->
  verify sha256 vs hub manifest (abort on mismatch) -> install non-root felhom-agent user + binary +
  sudoers (visudo -cf) + canonical unit. Idempotent.
- new step 7/8 golden: local fallback else fetch+verify+import from Gitea (--force-gitea-golden).
- agent now runs non-root (privileged.mode sudo), config chowned to the service user.
- README prerequisites trimmed to: install PVE + create customer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:38:26 +02:00

118 lines
4.1 KiB
Go

package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
// doArtifacts GETs /artifacts/{id} with the passphrase header (the do() helper only sets Bearer).
func doArtifacts(h *Handler, customerID, pw string) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodGet, "/api/v1/artifacts/"+customerID, nil)
if pw != "" {
req.Header.Set("X-Retrieval-Password", pw)
}
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
return rr
}
// Scenario A — a recorded manifest is returned verbatim.
func TestArtifacts_ReturnsRecordedSet(t *testing.T) {
h, st, _ := newTestHandler(t)
st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ckey", RetrievalPassword: "pass-phrase"})
if err := st.SetArtifactManifest(store.ArtifactManifest{
AgentVersion: "0.43.0",
AgentSHA256: strings.Repeat("a", 64),
GoldenVersion: "0.85.1",
GoldenSHA256: strings.Repeat("b", 64),
}); err != nil {
t.Fatalf("SetArtifactManifest: %v", err)
}
rr := doArtifacts(h, "c1", "pass-phrase")
if rr.Code != http.StatusOK {
t.Fatalf("status = %d, body=%s", rr.Code, rr.Body.String())
}
var got artifactManifestResponse
if err := json.Unmarshal(rr.Body.Bytes(), &got); err != nil {
t.Fatalf("decode: %v", err)
}
if got.Agent.Version != "0.43.0" || got.Agent.SHA256 != strings.Repeat("a", 64) {
t.Errorf("agent = %+v", got.Agent)
}
if got.Golden.Version != "0.85.1" || got.Golden.SHA256 != strings.Repeat("b", 64) {
t.Errorf("golden = %+v", got.Golden)
}
}
// Scenario B — an unset manifest returns 200 with empty fields (NOT an error). The script falls back
// to the local golden / fails clearly on a missing binary.
func TestArtifacts_EmptyWhenUnset(t *testing.T) {
h, st, _ := newTestHandler(t)
st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ckey", RetrievalPassword: "pass-phrase"})
rr := doArtifacts(h, "c1", "pass-phrase")
if rr.Code != http.StatusOK {
t.Fatalf("status = %d, body=%s", rr.Code, rr.Body.String())
}
var got artifactManifestResponse
if err := json.Unmarshal(rr.Body.Bytes(), &got); err != nil {
t.Fatalf("decode: %v", err)
}
if got.Agent.Version != "" || got.Agent.SHA256 != "" || got.Golden.Version != "" || got.Golden.SHA256 != "" {
t.Errorf("expected all-empty manifest, got %+v", got)
}
}
// Scenario C — wrong passphrase is 401 (auth mirrors config-retrieve).
func TestArtifacts_WrongPassphrase(t *testing.T) {
h, st, _ := newTestHandler(t)
st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ckey", RetrievalPassword: "pass-phrase"})
rr := doArtifacts(h, "c1", "wrong")
if rr.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want 401, body=%s", rr.Code, rr.Body.String())
}
}
// Scenario D — missing passphrase header is 401.
func TestArtifacts_MissingPassphrase(t *testing.T) {
h, st, _ := newTestHandler(t)
st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ckey", RetrievalPassword: "pass-phrase"})
rr := doArtifacts(h, "c1", "")
if rr.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want 401, body=%s", rr.Code, rr.Body.String())
}
}
// Scenario E — unknown customer is 404 (before any auth comparison leaks).
func TestArtifacts_UnknownCustomer(t *testing.T) {
h, _, _ := newTestHandler(t)
rr := doArtifacts(h, "nope", "whatever")
if rr.Code != http.StatusNotFound {
t.Fatalf("status = %d, want 404, body=%s", rr.Code, rr.Body.String())
}
}
// Scenario F — store round-trip: a partial set (agent only) persists independently.
func TestArtifacts_PartialSetRoundTrips(t *testing.T) {
_, st, _ := newTestHandler(t)
if err := st.SetArtifactManifest(store.ArtifactManifest{AgentVersion: "0.43.0", AgentSHA256: strings.Repeat("c", 64)}); err != nil {
t.Fatalf("SetArtifactManifest: %v", err)
}
m := st.GetArtifactManifest()
if m.AgentVersion != "0.43.0" || m.AgentSHA256 != strings.Repeat("c", 64) {
t.Errorf("agent fields not round-tripped: %+v", m)
}
if m.GoldenVersion != "" || m.GoldenSHA256 != "" {
t.Errorf("golden fields should be empty: %+v", m)
}
}