package main import ( "encoding/json" "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") } }