Files
verdande/internal/httpapi/beacon_notice_test.go
T
kristianwindandClaude Opus 5 0cf5c8c281 feat(beacon): sig det uopfordret, og lad være med at fejle i tavshed
Beaconet er slået til som udgangspunkt. Det kan kun forsvares, hvis det
bliver sagt af sig selv — og det blev det ikke. Det stod rigtigt og
fyldestgørende under Indstillinger → Data, som er en side, der bliver
åbnet af dem, der i forvejen har en mistanke om, at der er noget at læse.
For alle andre var "du kan slå den fra" et tilbud til nogen, der ikke
vidste, der var noget at slå fra.

Nu møder en administrator beskeden første gang: hvad der bliver sendt,
med de rigtige værdier i — det rigtige id, den rigtige version, den
rigtige adresse — og to knapper. Værdierne står der frem for ordet
"anonymt", fordi et løfte om telemetri er præcis så meget værd som
læserens mulighed for at efterprøve det.

`keep` er en peger og ikke en bool. En tom krop måtte ikke kunne betyde
"slå det fra": et fravalg, ingen har truffet, er lige så meget et brudt
løfte som et tilvalg, ingen har truffet.

Og tre ting mere, som Yggdrasil har lært på den hårde måde:

Et beacon, der ikke kan nå sin collector, fejlede i fuldstændig tavshed.
Det prøvede igen i morgen, siden sagde "sidst sendt" med en dato, der
bare blev ældre, og ingen opdagede det. Grunden bliver nu skrevet ned og
vist.

En installation, der opdaterer sig selv om natten, meldte gårsdagens
version i op til et døgn, fordi døgnet blev talt fra sidste vellykkede
melding. En version, der er skiftet, sender nu med det samme.

Og det offentliggjorte tal har fået et gulv. Et tal, der falder, kan ikke
offentliggøres som en kendsgerning — en dårlig udrulning, en
DNS-ændring, der stille stopper meldingerne, og rækker, der er væk, ser
alle sammen ud som "færre bruger det nu".

Adressen bliver stående: Verdande melder til verdande.yggdrasilpanel.com,
ikke til panelets egen collector. De to tæller hver sin slags
installation, og en fælles tabel har intet felt til at holde dem ude fra
hinanden.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 14:39:14 +02:00

133 lines
4.7 KiB
Go

package httpapi
import (
"net/http"
"testing"
)
// Beaconet er slået til som udgangspunkt, og det kan kun forsvares, hvis det
// bliver sagt uopfordret. Beskeden venter, indtil nogen har taget stilling — og
// den viser de rigtige værdier, så løftet kan efterprøves frem for tros på.
func TestTheBeaconSaysWhatItSendsBeforeAnybodyAsks(t *testing.T) {
ts := newTestServer(t)
ts.bootstrap(t)
resp, notice := ts.do(t, "GET", "/api/v1/beacon/notice", nil)
if resp.StatusCode != http.StatusOK {
t.Fatalf("hent besked: %d", resp.StatusCode)
}
if notice["pending"] != true {
t.Fatalf("beskeden er ikke i vente på en frisk installation: %v", notice)
}
if notice["enabled"] != true {
t.Errorf("beaconet er ikke slået til som udgangspunkt: %v", notice)
}
// De tre værdier, beskeden viser frem, er de rigtige — ikke et eksempel.
settings := beaconSettingsOf(t, ts)
if notice["instance_id"] != settings["instance_id"] {
t.Errorf("id i beskeden = %v, i indstillingerne = %v",
notice["instance_id"], settings["instance_id"])
}
if notice["collector_url"] != settings["collector_url"] {
t.Errorf("adressen i beskeden = %v, i indstillingerne = %v",
notice["collector_url"], settings["collector_url"])
}
if notice["version"] == "" || notice["version"] == nil {
t.Error("beskeden siger ikke, hvilken version der bliver sendt")
}
}
// "Behold den" er et svar, og det svar skal huskes: beskeden må ikke komme igen
// ved næste login.
func TestKeepingTheBeaconAnswersTheNoticeForGood(t *testing.T) {
ts := newTestServer(t)
ts.bootstrap(t)
resp, out := ts.do(t, "POST", "/api/v1/beacon/notice", map[string]any{"keep": true})
if resp.StatusCode != http.StatusOK || out["enabled"] != true {
t.Fatalf("behold: %d %v", resp.StatusCode, out)
}
if _, notice := ts.do(t, "GET", "/api/v1/beacon/notice", nil); notice["pending"] != false {
t.Errorf("beskeden kommer igen efter et svar: %v", notice)
}
if s := beaconSettingsOf(t, ts); s["enabled"] != true {
t.Errorf("beaconet blev slået fra af et ja: %v", s["enabled"])
}
}
// Og "slå den fra" slår den fra — ét kald, både svaret og virkningen.
func TestTurningTheBeaconOffFromTheNoticeStopsIt(t *testing.T) {
ts := newTestServer(t)
ts.bootstrap(t)
resp, out := ts.do(t, "POST", "/api/v1/beacon/notice", map[string]any{"keep": false})
if resp.StatusCode != http.StatusOK || out["enabled"] != false {
t.Fatalf("slå fra: %d %v", resp.StatusCode, out)
}
s := beaconSettingsOf(t, ts)
if s["enabled"] != false {
t.Errorf("beaconet kører stadig efter et nej: %v", s["enabled"])
}
if _, notice := ts.do(t, "GET", "/api/v1/beacon/notice", nil); notice["pending"] != false {
t.Errorf("beskeden kommer igen efter et nej: %v", notice)
}
}
// En tom krop må ikke kunne betyde "slå det fra". Et fravalg, ingen har truffet,
// er lige så meget et brudt løfte som et tilvalg, ingen har truffet.
func TestAnEmptyAnswerToTheNoticeIsRefused(t *testing.T) {
ts := newTestServer(t)
ts.bootstrap(t)
resp, _ := ts.do(t, "POST", "/api/v1/beacon/notice", map[string]any{})
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("tom krop gav %d, want 422", resp.StatusCode)
}
if s := beaconSettingsOf(t, ts); s["enabled"] != true {
t.Errorf("en tom krop slog beaconet fra: %v", s["enabled"])
}
if _, notice := ts.do(t, "GET", "/api/v1/beacon/notice", nil); notice["pending"] != true {
t.Errorf("en tom krop talte som et svar: %v", notice)
}
}
// Et tal, der falder, kan ikke offentliggøres som en kendsgerning. Under gulvet
// svares der ingenting frem for et lille tal, ingen kan tolke.
func TestThePublishedCountHasAFloorUnderIt(t *testing.T) {
ts := newTestServer(t)
ts.bootstrap(t)
if resp, _ := ts.do(t, "PUT", "/api/v1/beacon/settings", map[string]any{
"is_collector": true, "publish_count": true,
}); resp.StatusCode != http.StatusOK {
t.Fatal("bliv collector")
}
// Instansen tæller sig selv, så der er én. Gulvet er højere.
resp, out := ts.do(t, "GET", "/api/v1/beacon/count", nil)
if resp.StatusCode != http.StatusOK {
t.Fatalf("tælling: %d", resp.StatusCode)
}
if out["installs"] != nil {
t.Errorf("et tal under gulvet blev offentliggjort: %v", out["installs"])
}
// Sættes gulvet ned, står tallet der.
if resp, _ := ts.do(t, "PUT", "/api/v1/beacon/settings",
map[string]any{"publish_min": 0}); resp.StatusCode != http.StatusOK {
t.Fatal("sænk gulvet")
}
if _, out := ts.do(t, "GET", "/api/v1/beacon/count", nil); out["installs"] == nil {
t.Errorf("tallet blev holdt tilbage uden et gulv: %v", out)
}
}
func beaconSettingsOf(t *testing.T, ts *testServer) map[string]any {
t.Helper()
resp, out := ts.do(t, "GET", "/api/v1/beacon/settings", nil)
if resp.StatusCode != http.StatusOK {
t.Fatalf("hent beacon-indstillinger: %d", resp.StatusCode)
}
return out
}