Files
memby/server/internal/api/update_test.go

186 lines
6.0 KiB
Go

package api
import (
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"github.com/ponzischeme89/memby/server/internal/appupdate"
"github.com/ponzischeme89/memby/server/internal/config"
serverlogging "github.com/ponzischeme89/memby/server/internal/logging"
"github.com/ponzischeme89/memby/server/internal/store"
)
func TestUpdateStatusIsPublicAndAvailableDuringMaintenance(t *testing.T) {
server := testServer(config.Config{})
server.updatePolicy.set(appupdate.Policy{
Enabled: true,
LatestVersion: "0.3.0",
MinimumVersion: "0.2.9",
DownloadURL: "/updates/memby-0.3.0.apk?token=signed",
})
server.maintenance.set(store.Maintenance{Enabled: true, Message: "Deploying"})
req := httptest.NewRequest(http.MethodGet, "/v1/update", nil)
req.Header.Set("X-Memby-Version", "0.2.8")
rec := httptest.NewRecorder()
// Deliberately no Authorization header: update policy must precede login.
server.Routes().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body = %s", rec.Code, rec.Body.String())
}
var decision appupdate.Decision
if err := json.Unmarshal(rec.Body.Bytes(), &decision); err != nil {
t.Fatalf("decode decision: %v", err)
}
if decision.Status != appupdate.StatusMandatory {
t.Fatalf("status = %q, want mandatory", decision.Status)
}
}
func TestUpdateOfferLogNamesTheAffectedViewer(t *testing.T) {
logger, events := serverlogging.NewBuffered(io.Discard, slog.LevelInfo, 10, serverlogging.FormatConsole)
server := New(config.Config{}, Deps{Log: logger, Events: events})
server.updatePolicy.set(appupdate.Policy{
Enabled: true,
LatestVersion: "0.2.44",
DownloadURL: "/updates/memby-0.2.44.apk",
})
req := httptest.NewRequest(http.MethodGet, "/v1/update", nil)
req.Header.Set("X-Memby-Version", "0.2.38")
req, _ = withRequestIdentity(req)
identify(req.Context(), store.Session{Username: "matt", DeviceName: "Living room"})
server.handleUpdate(httptest.NewRecorder(), req)
page := events.Events(0, 10)
if len(page.Events) != 1 {
t.Fatalf("events = %d, want 1", len(page.Events))
}
event := page.Events[0]
if event.Message != "update offered" || event.Attributes["user"] != "matt" {
t.Fatalf("update event was not attributed to the viewer: %+v", event)
}
}
func TestConfiguredFloorForcesClientsBelow0244(t *testing.T) {
server := testServer(config.Config{})
server.updatePolicy.set(appupdate.Policy{
Enabled: true,
LatestVersion: "0.2.44",
RetireBelowVersion: "0.2.44",
DownloadURL: "/updates/memby-0.2.44.apk?token=signed",
})
for version, want := range map[string]string{
"0.2.43": appupdate.StatusMandatory,
"0.2.44": appupdate.StatusNone,
"0.2.45": appupdate.StatusNone,
} {
req := httptest.NewRequest(http.MethodGet, "/v1/update", nil)
req.Header.Set("X-Memby-Version", version)
rec := httptest.NewRecorder()
server.handleUpdate(rec, req)
var decision appupdate.Decision
if err := json.Unmarshal(rec.Body.Bytes(), &decision); err != nil {
t.Fatalf("%s: decode decision: %v", version, err)
}
if decision.Status != want {
t.Errorf("%s: status = %q, want %q", version, decision.Status, want)
}
}
}
func TestConfiguredFloorWaitsForAnActionableRelease(t *testing.T) {
for name, policy := range map[string]appupdate.Policy{
"disabled": {
LatestVersion: "0.2.44", RetireBelowVersion: "0.2.44",
DownloadURL: "/updates/memby-0.2.44.apk",
},
"missing download": {
Enabled: true, LatestVersion: "0.2.44", RetireBelowVersion: "0.2.44",
},
"release too old": {
Enabled: true, LatestVersion: "0.2.43", RetireBelowVersion: "0.2.44",
DownloadURL: "/updates/memby-0.2.43.apk",
},
} {
t.Run(name, func(t *testing.T) {
decision := appupdate.Decide(effectiveUpdatePolicy(policy), "0.2.43")
if mustRetireForUpdate(decision, "0.2.43", destructiveUpdateFloor(policy)) {
t.Fatal("client would be retired without an actionable release at the configured floor")
}
})
}
}
func TestSelectedDestructiveFloorRetiresClientsBelowRelease(t *testing.T) {
policy := appupdate.Policy{
Enabled: true,
LatestVersion: "0.3.0",
RetireBelowVersion: "0.3.0",
DownloadURL: "/updates/memby-0.3.0.apk?token=signed",
}
for version, want := range map[string]bool{
"0.2.99": true,
"0.3.0": false,
"0.3.1": false,
} {
decision := appupdate.Decide(effectiveUpdatePolicy(policy), version)
if got := mustRetireForUpdate(decision, version, destructiveUpdateFloor(policy)); got != want {
t.Errorf("%s: retired = %t, want %t", version, got, want)
}
}
}
func TestRequiredUpdateDoesNotRetireSupportedClient(t *testing.T) {
policy := appupdate.Policy{
Enabled: true,
LatestVersion: "0.3.0",
MinimumVersion: "0.3.0",
DownloadURL: "/updates/memby-0.3.0.apk?token=signed",
}
decision := appupdate.Decide(effectiveUpdatePolicy(policy), "0.2.44")
if decision.Status != appupdate.StatusMandatory {
t.Fatalf("status = %q, want mandatory", decision.Status)
}
if mustRetireForUpdate(decision, "0.2.44", destructiveUpdateFloor(policy)) {
t.Fatal("ordinary required update retired a supported client")
}
}
func TestRetiredClientCannotSignBackIn(t *testing.T) {
server := testServer(config.Config{})
server.updatePolicy.set(appupdate.Policy{
Enabled: true,
LatestVersion: "0.2.44",
RetireBelowVersion: "0.2.44",
DownloadURL: "/updates/memby-0.2.44.apk?token=signed",
})
reached := false
handler := server.requireSupportedClient(func(http.ResponseWriter, *http.Request) {
reached = true
})
req := httptest.NewRequest(http.MethodPost, "/v1/auth/login", nil)
req.Header.Set("X-Memby-Version", "0.2.43")
rec := httptest.NewRecorder()
handler(rec, req)
if reached {
t.Fatal("retired client reached the login handler")
}
if rec.Code != http.StatusUpgradeRequired {
t.Fatalf("status = %d, want 426", rec.Code)
}
if rec.Header().Get("X-Memby-Update-Required") != "0.2.44" {
t.Fatalf("required update header = %q", rec.Header().Get("X-Memby-Update-Required"))
}
}