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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
445d9de57a
commit
0cf5c8c281
@@ -20,6 +20,11 @@ import (
|
||||
|
||||
const beaconScope = "beacon"
|
||||
|
||||
// defaultPublishMin er, hvor mange installationer der skal være, før tallet
|
||||
// offentliggøres. Selve tallet er et skøn; det, gulvet køber, er det ikke: under
|
||||
// det fortæller et offentligt tal mest om, hvor lidt der skal til at flytte det.
|
||||
const defaultPublishMin = 25
|
||||
|
||||
// beaconConfig is what this instance has been told to do. The zero value is what
|
||||
// a fresh installation gets, which is why Enabled defaults to true elsewhere and
|
||||
// not here — a zero struct must never mean "silently on".
|
||||
@@ -30,6 +35,33 @@ type beaconConfig struct {
|
||||
IsCollector bool `json:"is_collector"`
|
||||
PublishCount bool `json:"publish_count"`
|
||||
LastPingAt int64 `json:"last_ping_at,omitempty"`
|
||||
// LastVersion er den version, der sidst nåede frem. Gemt, fordi en
|
||||
// installation, der opdaterer sig selv om natten, ellers står som gårsdagens
|
||||
// version i op til et døgn — og en tælling efter version, der er en dag bagud,
|
||||
// kan man ikke se en udrulning i.
|
||||
LastVersion string `json:"last_version,omitempty"`
|
||||
// LastError er grunden til, at det sidste forsøg ikke nåede frem.
|
||||
//
|
||||
// Skrevet ned, fordi alternativet er tavshed: et beacon, der ikke kan nå sin
|
||||
// collector, prøver igen i morgen og siger ingenting, og det opdages først ved
|
||||
// at tælle installationer i den anden ende og savne en. Det er ikke en fejl,
|
||||
// der skal afbryde nogen — det er en linje på en side, der ellers påstår, at
|
||||
// alt er sendt.
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
LastErrorAt int64 `json:"last_error_at,omitempty"`
|
||||
// NoticeAck er sat, når nogen har set beskeden om, hvad der bliver sendt.
|
||||
//
|
||||
// Slået til som udgangspunkt kan kun forsvares, hvis det bliver sagt
|
||||
// uopfordret — ikke på en indstillingsside, ingen åbner af sig selv. Feltet er
|
||||
// det, der husker, at det er blevet sagt.
|
||||
NoticeAck bool `json:"notice_ack"`
|
||||
// PublishMin er gulvet under det offentliggjorte tal.
|
||||
//
|
||||
// Et tal, der *falder*, kan ikke offentliggøres som en kendsgerning: en dårlig
|
||||
// udrulning, en DNS-ændring, der stille stopper meldingerne, eller rækker, der
|
||||
// er væk, ser alle sammen ud som "færre bruger det nu". Under gulvet svares
|
||||
// der ingenting frem for et lille tal, ingen kan tolke.
|
||||
PublishMin int `json:"publish_min"`
|
||||
}
|
||||
|
||||
// beaconSettings reads the configuration, filling in the defaults and minting the
|
||||
@@ -40,7 +72,9 @@ type beaconConfig struct {
|
||||
// also be a value the collector could compare against a guess — which is the
|
||||
// difference between anonymous and pseudonymous, and it is the whole point.
|
||||
func (s *Server) beaconSettings(ctx context.Context) (beaconConfig, error) {
|
||||
cfg := beaconConfig{Enabled: true, Collector: beacon.DefaultCollector}
|
||||
cfg := beaconConfig{
|
||||
Enabled: true, Collector: beacon.DefaultCollector, PublishMin: defaultPublishMin,
|
||||
}
|
||||
|
||||
stored, err := s.db.InstanceSettings(ctx, beaconScope)
|
||||
if err != nil {
|
||||
@@ -65,6 +99,21 @@ func (s *Server) beaconSettings(ctx context.Context) (beaconConfig, error) {
|
||||
if v, ok := stored["last_ping_at"].(float64); ok {
|
||||
cfg.LastPingAt = int64(v)
|
||||
}
|
||||
if v, ok := stored["last_version"].(string); ok {
|
||||
cfg.LastVersion = v
|
||||
}
|
||||
if v, ok := stored["last_error"].(string); ok {
|
||||
cfg.LastError = v
|
||||
}
|
||||
if v, ok := stored["last_error_at"].(float64); ok {
|
||||
cfg.LastErrorAt = int64(v)
|
||||
}
|
||||
if v, ok := stored["notice_ack"].(bool); ok {
|
||||
cfg.NoticeAck = v
|
||||
}
|
||||
if v, ok := stored["publish_min"].(float64); ok && v >= 0 {
|
||||
cfg.PublishMin = int(v)
|
||||
}
|
||||
|
||||
if cfg.InstanceID == "" {
|
||||
cfg.InstanceID = store.NewID()
|
||||
@@ -83,20 +132,31 @@ func (s *Server) saveBeaconSettings(ctx context.Context, cfg beaconConfig) error
|
||||
"is_collector": cfg.IsCollector,
|
||||
"publish_count": cfg.PublishCount,
|
||||
"last_ping_at": cfg.LastPingAt,
|
||||
"last_version": cfg.LastVersion,
|
||||
"last_error": cfg.LastError,
|
||||
"last_error_at": cfg.LastErrorAt,
|
||||
"notice_ack": cfg.NoticeAck,
|
||||
"publish_min": cfg.PublishMin,
|
||||
})
|
||||
}
|
||||
|
||||
// --- what the settings page shows ------------------------------------------------
|
||||
|
||||
type beaconStatusJSON struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Collector string `json:"collector_url"`
|
||||
InstanceID string `json:"instance_id"`
|
||||
Version string `json:"version"`
|
||||
IsCollector bool `json:"is_collector"`
|
||||
PublishCount bool `json:"publish_count"`
|
||||
LastPingAt string `json:"last_ping_at,omitempty"`
|
||||
Stats *store.BeaconStats `json:"stats,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Collector string `json:"collector_url"`
|
||||
InstanceID string `json:"instance_id"`
|
||||
Version string `json:"version"`
|
||||
IsCollector bool `json:"is_collector"`
|
||||
PublishCount bool `json:"publish_count"`
|
||||
PublishMin int `json:"publish_min"`
|
||||
LastPingAt string `json:"last_ping_at,omitempty"`
|
||||
// Sidste forsøg, der ikke nåede frem — tomt, når det sidste gik godt. Vist
|
||||
// frem for talt: en dato under "sidst sendt", der bliver ældre og ældre, er
|
||||
// det eneste spor, et tavst beacon ellers efterlader.
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
LastErrorAt string `json:"last_error_at,omitempty"`
|
||||
Stats *store.BeaconStats `json:"stats,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) handleBeaconStatus(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -109,10 +169,15 @@ func (s *Server) handleBeaconStatus(w http.ResponseWriter, r *http.Request) {
|
||||
out := beaconStatusJSON{
|
||||
Enabled: cfg.Enabled, Collector: cfg.Collector, InstanceID: cfg.InstanceID,
|
||||
Version: Version, IsCollector: cfg.IsCollector, PublishCount: cfg.PublishCount,
|
||||
PublishMin: cfg.PublishMin,
|
||||
}
|
||||
if cfg.LastPingAt > 0 {
|
||||
out.LastPingAt = time.Unix(cfg.LastPingAt, 0).Format(time.RFC3339)
|
||||
}
|
||||
if cfg.LastError != "" {
|
||||
out.LastError = cfg.LastError
|
||||
out.LastErrorAt = time.Unix(cfg.LastErrorAt, 0).Format(time.RFC3339)
|
||||
}
|
||||
if cfg.IsCollector {
|
||||
stats, err := s.db.BeaconSummary(r.Context())
|
||||
if err != nil {
|
||||
@@ -129,6 +194,7 @@ type beaconRequest struct {
|
||||
Collector *string `json:"collector_url"`
|
||||
IsCollector *bool `json:"is_collector"`
|
||||
PublishCount *bool `json:"publish_count"`
|
||||
PublishMin *int `json:"publish_min"`
|
||||
}
|
||||
|
||||
func (s *Server) handleSetBeacon(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -151,6 +217,13 @@ func (s *Server) handleSetBeacon(w http.ResponseWriter, r *http.Request) {
|
||||
if req.PublishCount != nil {
|
||||
cfg.PublishCount = *req.PublishCount
|
||||
}
|
||||
if req.PublishMin != nil {
|
||||
if *req.PublishMin < 0 {
|
||||
writeFieldErrors(w, map[string]string{"publish_min": "must not be negative"})
|
||||
return
|
||||
}
|
||||
cfg.PublishMin = *req.PublishMin
|
||||
}
|
||||
if req.Collector != nil {
|
||||
url := strings.TrimSpace(*req.Collector)
|
||||
if url == "" {
|
||||
@@ -237,6 +310,16 @@ func (s *Server) handleBeaconCount(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
n, err := s.db.BeaconCount(r.Context(), 30*24*time.Hour)
|
||||
if err == nil && n < cfg.PublishMin {
|
||||
// Ingenting frem for et lille tal. Et tal under gulvet fortæller mest om,
|
||||
// hvor lidt der skal til at flytte det — og et, der falder, ville blive
|
||||
// læst som "færre bruger det nu", også når det i virkeligheden var en
|
||||
// udrulning, der gik galt, eller en adresse, der stille holdt op med at
|
||||
// blive ramt.
|
||||
w.Header().Set("Cache-Control", "public, max-age=60")
|
||||
writeJSON(w, http.StatusOK, map[string]any{"installs": nil, "window_days": 30})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
s.internal(w, r, "beacon count", err)
|
||||
return
|
||||
@@ -269,11 +352,15 @@ func (s *Server) SendBeacon(ctx context.Context) error {
|
||||
if err := s.db.RecordBeacon(ctx, cfg.InstanceID, Version); err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.LastPingAt = time.Now().Unix()
|
||||
return s.saveBeaconSettings(ctx, cfg)
|
||||
return s.beaconSent(ctx, cfg)
|
||||
}
|
||||
|
||||
if time.Since(time.Unix(cfg.LastPingAt, 0)) < 24*time.Hour {
|
||||
// Undtagelsen fra døgnet er en version, der er skiftet. En installation, der
|
||||
// opdaterer sig selv om natten og først melder ind igen et døgn senere, står
|
||||
// som gårsdagens version hele dagen — og så kan man ikke se en udrulning i
|
||||
// tallene. Målt hos Yggdrasil: to paneler rapporterede den forrige version i
|
||||
// et døgn efter at være opdateret.
|
||||
if time.Since(time.Unix(cfg.LastPingAt, 0)) < 24*time.Hour && cfg.LastVersion == Version {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -289,9 +376,100 @@ func (s *Server) SendBeacon(ctx context.Context) error {
|
||||
// installation's problem and must never look like one — it is somebody
|
||||
// else's server, and the only consequence is a number being one lower.
|
||||
s.log.Info("beacon not delivered", "err", err)
|
||||
return nil
|
||||
// Men skrevet ned. Uden det her fejler et beacon, der ikke kan nå sin
|
||||
// collector, i fuldstændig tavshed: det prøver igen i morgen, siden siger
|
||||
// "sidst sendt" med en dato, der bare bliver ældre, og ingen opdager det.
|
||||
// Fundet hos Yggdrasil ved at tælle installationer i den anden ende og
|
||||
// savne en.
|
||||
cfg.LastError, cfg.LastErrorAt = err.Error(), time.Now().Unix()
|
||||
return s.saveBeaconSettings(ctx, cfg)
|
||||
}
|
||||
|
||||
return s.beaconSent(ctx, cfg)
|
||||
}
|
||||
|
||||
// beaconSent skriver, at det lykkedes: tidspunktet, versionen der nåede frem, og
|
||||
// en fejl, der ikke længere gælder. Ét sted, fordi de fire felter skal skrives
|
||||
// sammen — en gemning, der glemte at rydde fejlen, ville lade en linje stå på
|
||||
// siden om noget, der virker.
|
||||
func (s *Server) beaconSent(ctx context.Context, cfg beaconConfig) error {
|
||||
cfg.LastPingAt = time.Now().Unix()
|
||||
cfg.LastVersion = Version
|
||||
cfg.LastError, cfg.LastErrorAt = "", 0
|
||||
return s.saveBeaconSettings(ctx, cfg)
|
||||
}
|
||||
|
||||
// --- beskeden, der bliver sagt uopfordret ------------------------------------------
|
||||
|
||||
// handleBeaconNotice says whether the disclosure is still owed, and what it would
|
||||
// say.
|
||||
//
|
||||
// Slået til som udgangspunkt kan kun forsvares, hvis det bliver sagt uopfordret.
|
||||
// En indstillingsside, hvor det står rigtigt og fyldestgørende, er ikke det samme
|
||||
// som at sige det: den bliver åbnet af dem, der i forvejen har en mistanke om, at
|
||||
// der er noget at læse. Uden beskeden her er "du kan slå den fra" et tilbud til
|
||||
// dem, der ved, at der er noget at slå fra.
|
||||
//
|
||||
// De tre værdier står i svaret — det rigtige id, den rigtige version, den rigtige
|
||||
// adresse — så beskeden kan vise, hvad der bliver sendt, frem for at sige
|
||||
// "anonyme data" og bede om at blive troet på. Det er den eneste version af det
|
||||
// løfte, der er noget værd: en, læseren selv kan kontrollere.
|
||||
//
|
||||
// Panelbredt frem for pr. bruger: det er installationen, der melder ind, ikke
|
||||
// personen, og en besked pr. konto ville stille det samme spørgsmål til folk, der
|
||||
// ikke kan svare på det.
|
||||
func (s *Server) handleBeaconNotice(w http.ResponseWriter, r *http.Request) {
|
||||
cfg, err := s.beaconSettings(r.Context())
|
||||
if err != nil {
|
||||
s.internal(w, r, "beacon settings", err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"pending": !cfg.NoticeAck,
|
||||
"enabled": cfg.Enabled,
|
||||
"instance_id": cfg.InstanceID,
|
||||
"version": Version,
|
||||
"collector_url": cfg.Collector,
|
||||
})
|
||||
}
|
||||
|
||||
type beaconNoticeRequest struct {
|
||||
// Keep er en peger og ikke en bool, og det er hele pointen.
|
||||
//
|
||||
// En tom krop ville ellers betyde "slå det fra", og det er den vej, en fejl
|
||||
// aldrig må falde: et fravalg, ingen har truffet, er lige så meget et brudt
|
||||
// løfte som et tilvalg, ingen har truffet. Yggdrasil har den fejl i sin
|
||||
// tilsvarende ende og er blevet bidt af den.
|
||||
Keep *bool `json:"keep"`
|
||||
}
|
||||
|
||||
// handleAckBeaconNotice records that the notice has been seen, and turns the
|
||||
// beacon off if that was the answer.
|
||||
//
|
||||
// Ét kald til begge svar. "Behold den" og "slå den fra" er den samme handling —
|
||||
// nogen har taget stilling — og to endpoints ville betyde, at det ene kunne
|
||||
// glemme at skrive, at der var taget stilling, og beskeden dermed komme igen.
|
||||
func (s *Server) handleAckBeaconNotice(w http.ResponseWriter, r *http.Request) {
|
||||
var req beaconNoticeRequest
|
||||
if err := decodeJSON(w, r, &req); err != nil {
|
||||
return
|
||||
}
|
||||
if req.Keep == nil {
|
||||
writeFieldErrors(w, map[string]string{"keep": "required"})
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := s.beaconSettings(r.Context())
|
||||
if err != nil {
|
||||
s.internal(w, r, "beacon settings", err)
|
||||
return
|
||||
}
|
||||
cfg.NoticeAck = true
|
||||
cfg.Enabled = *req.Keep
|
||||
if err := s.saveBeaconSettings(r.Context(), cfg); err != nil {
|
||||
s.internal(w, r, "save beacon settings", err)
|
||||
return
|
||||
}
|
||||
s.log.Info("beacon notice answered", "keep", *req.Keep, "user", userFrom(r.Context()).ID)
|
||||
writeJSON(w, http.StatusOK, map[string]any{"enabled": cfg.Enabled})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
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
|
||||
}
|
||||
@@ -512,6 +512,12 @@ func New(cfg *config.Config, db *store.DB, log *slog.Logger, web fs.FS) *Server
|
||||
// installation and it either phones home or it does not.
|
||||
r.Get("/beacon/settings", s.handleBeaconStatus)
|
||||
r.Put("/beacon/settings", s.handleSetBeacon)
|
||||
// Beskeden om, hvad der bliver sendt, og svaret på den. I samme
|
||||
// gruppe som resten: den, der kan slå beaconet fra, er den, der
|
||||
// skal have beskeden, og at spørge alle andre ville være at
|
||||
// stille et spørgsmål, de ikke kan svare på.
|
||||
r.Get("/beacon/notice", s.handleBeaconNotice)
|
||||
r.Post("/beacon/notice", s.handleAckBeaconNotice)
|
||||
// A backup file is a complete copy of the database, so this whole
|
||||
// group is sessions-only as well as administrators-only: a leaked
|
||||
// token must not be able to download everybody's data.
|
||||
|
||||
Reference in New Issue
Block a user