Files
mg4/server/releases_test.go
T
nilsandClaude Opus 5.5 acb7e1969e Server 2.6.2: log update checks and downloads; Go 1.27.3
The update relay logs every check (car, number of releases, newest
pre-release) and every download (car, file, size, then bytes and time,
or that it was cut short); never the forge's address or token, tested.
Go 1.27.3 for the build image and toolchain: govulncheck had found
GO-2026-6809 in crypto/x509 under 1.26.9; source and binary now clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
2026-10-25 22:40:23 +01:00

151 lines
5.5 KiB
Go

package main
import (
"bytes"
"encoding/json"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
const testForgeToken = "forge-read-token-0123456789"
// fakeForge answers like Gitea for one pre-release with an APK and its sidecar, and only to
// the right token.
func fakeForge(t *testing.T) *httptest.Server {
t.Helper()
mux := http.NewServeMux()
auth := func(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "token "+testForgeToken {
http.NotFound(w, r) // a private repository looks absent without a login
return
}
h(w, r)
}
}
mux.HandleFunc("GET /api/v1/repos/nils/mg4/releases", auth(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`[{"id":7,"tag_name":"car-2.16.0","prerelease":true,"draft":false,"assets":[
{"id":11,"name":"NilsMG4-unstable-2.16.0.apk","size":4},
{"id":12,"name":"NilsMG4-unstable-2.16.0.apk.sha256","size":64}]}]`))
}))
mux.HandleFunc("GET /api/v1/repos/nils/mg4/releases/7/assets/11", auth(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"id":11,"name":"NilsMG4-unstable-2.16.0.apk","uuid":"abc-uuid"}`))
}))
mux.HandleFunc("GET /attachments/abc-uuid", auth(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("APK!"))
}))
return httptest.NewTLSServer(mux)
}
func releasesAPI(t *testing.T, forge *httptest.Server) http.Handler {
t.Helper()
api, _ := newTestAPI()
api.Releases = &Releases{ForgeURL: forge.URL, Repo: "nils/mg4", Token: testForgeToken, Client: forge.Client()}
return api.Routes()
}
func TestReleasesAreRelayedWithDownloadsThroughTheServer(t *testing.T) {
forge := fakeForge(t)
defer forge.Close()
h := releasesAPI(t, forge)
if rec := do(t, h, "GET", "/v1/app/releases", "", ""); rec.Code != 401 {
t.Fatalf("without the car's token: %d", rec.Code)
}
rec := do(t, h, "GET", "/v1/app/releases", testToken, "")
if rec.Code != 200 {
t.Fatalf("%d %s", rec.Code, rec.Body)
}
var got []carRelease
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatal(err)
}
if len(got) != 1 || !got[0].Prerelease || len(got[0].Assets) != 2 ||
got[0].Assets[0].BrowserDownloadURL != "https://car.example.com/v1/app/assets/7/11/NilsMG4-unstable-2.16.0.apk" {
t.Fatalf("releases %+v", got)
}
if strings.Contains(rec.Body.String(), testForgeToken) || strings.Contains(rec.Body.String(), forge.URL) {
t.Fatal("the forge or its token leaked to the car")
}
file := do(t, h, "GET", "/v1/app/assets/7/11/NilsMG4-unstable-2.16.0.apk", testToken, "")
if file.Code != 200 || file.Body.String() != "APK!" {
t.Fatalf("download %d %q", file.Code, file.Body)
}
// The name must be the asset's own: no asking for one file under another's name.
if rec := do(t, h, "GET", "/v1/app/assets/7/11/other.apk", testToken, ""); rec.Code != 404 {
t.Fatalf("wrong name: %d", rec.Code)
}
}
func TestAWrongForgeTokenIsABadGatewayNotALeak(t *testing.T) {
forge := fakeForge(t)
defer forge.Close()
api, _ := newTestAPI()
api.Releases = &Releases{ForgeURL: forge.URL, Repo: "nils/mg4", Token: "wrong", Client: forge.Client()}
if rec := do(t, api.Routes(), "GET", "/v1/app/releases", testToken, ""); rec.Code != 502 {
t.Fatalf("%d", rec.Code)
}
}
func TestAppUpdatesAreOffWithoutAForge(t *testing.T) {
api, _ := newTestAPI()
if rec := do(t, api.Routes(), "GET", "/v1/app/releases", testToken, ""); rec.Code != 404 {
t.Fatalf("%d", rec.Code)
}
}
func TestRedirectsAwayFromTheForgeAreRefused(t *testing.T) {
rel := &Releases{ForgeURL: "https://git.example.com"}
away, _ := http.NewRequest("GET", "https://elsewhere.example.com/x", nil)
if rel.sameHostOnly(away, nil) == nil {
t.Fatal("redirect to another host followed")
}
same, _ := http.NewRequest("GET", "https://git.example.com/attachments/x", nil)
if err := rel.sameHostOnly(same, nil); err != nil {
t.Fatal(err)
}
}
func TestForgeURLMustBeHTTPS(t *testing.T) {
env := map[string]string{"INFLUX_URL": "u", "INFLUX_TOKEN": "t", "INFLUX_ORG": "o",
"INGEST_TOKEN": "0123456789abcdef01234567", "FORGE_URL": "http://git.example.com"}
if _, err := loadConfig(func(k string) string { return env[k] }); err == nil {
t.Fatal("plain http forge accepted")
}
}
// The server's log says who checked for and fetched an update, and how it went — the car's
// installation (here the shared token), the newest pre-release, the file and the bytes — and
// never the forge's token or address.
func TestUpdatesAreLogged(t *testing.T) {
var logged bytes.Buffer
previous := slog.Default()
slog.SetDefault(slog.New(slog.NewTextHandler(&logged, nil)))
defer slog.SetDefault(previous)
forge := fakeForge(t)
defer forge.Close()
h := releasesAPI(t, forge)
if rec := do(t, h, "GET", "/v1/app/releases", testToken, ""); rec.Code != 200 {
t.Fatalf("%d", rec.Code)
}
if rec := do(t, h, "GET", "/v1/app/assets/7/11/NilsMG4-unstable-2.16.0.apk", testToken, ""); rec.Code != 200 {
t.Fatalf("%d", rec.Code)
}
out := logged.String()
for _, want := range []string{
`msg="update check" installation="shared token" releases=1 newest=car-2.16.0`,
`msg="update download started" installation="shared token" asset=NilsMG4-unstable-2.16.0.apk`,
`msg="update handed out" installation="shared token" asset=NilsMG4-unstable-2.16.0.apk sent=4`,
} {
if !strings.Contains(out, want) {
t.Errorf("log lacks %q:\n%s", want, out)
}
}
if strings.Contains(out, testForgeToken) || strings.Contains(out, forge.URL) {
t.Fatal("the forge or its token leaked into the log")
}
}