// Copyright (C) 2026 Dcntrlized // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package catalog import ( "net/http" "net/http/httptest" "strings" "testing" "git.dcntrlized.org/Dcntrlized/Maestro/lib/catalog" ) func serveDownload(t *testing.T, h *Handler, id, filename string) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodGet, "/api/catalog/artifacts/"+id+"/"+filename, nil) req.SetPathValue("id", id) req.SetPathValue("filename", filename) rec := httptest.NewRecorder() h.Download(rec, req) return rec } func TestDownloadHostedExactBytesAndHeaders(t *testing.T) { h, _ := newTestHandler(t) app := seedHostedApp(t, h, "com.example.download", "exact-apk-bytes") rec := serveDownload(t, h, app.ID, app.Artifact.Filename) if rec.Code != http.StatusOK { t.Fatalf("status = %d body %s", rec.Code, rec.Body.String()) } if rec.Body.String() != "exact-apk-bytes" { t.Fatalf("body = %q", rec.Body.String()) } if got := rec.Header().Get("Content-Length"); got != "15" { t.Fatalf("Content-Length = %q, want 15", got) } if got := rec.Header().Get("X-Content-SHA256"); got != app.Artifact.SHA256 { t.Fatalf("X-Content-SHA256 = %q, want %q", got, app.Artifact.SHA256) } if got := rec.Header().Get("Content-Type"); got != catalog.DefaultArtifactContentType { t.Fatalf("Content-Type = %q", got) } if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "*" { t.Fatalf("Access-Control-Allow-Origin = %q", got) } } func TestDownloadUnknownAndWrongFilename(t *testing.T) { h, _ := newTestHandler(t) app := seedHostedApp(t, h, "com.example.dl2", "bytes") if rec := serveDownload(t, h, app.ID, "other.apk"); rec.Code != http.StatusNotFound { t.Fatalf("wrong filename status = %d", rec.Code) } if rec := serveDownload(t, h, "a_missing", "app.apk"); rec.Code != http.StatusNotFound { t.Fatalf("unknown id status = %d", rec.Code) } } func TestDownloadLinkRedirects(t *testing.T) { h, _ := newTestHandler(t) link := "https://downloads.example.org/app.apk" app := catalog.App{ ID: catalog.AppID("com.example.redirect"), Package: "com.example.redirect", Name: "Link", VersionName: "1.0", Artifact: catalog.Artifact{ Filename: "app.apk", SHA256: strings.Repeat("c", 64), Kind: catalog.KindLink, SourceURL: link, }, } if err := h.store.Upsert(app); err != nil { t.Fatalf("seed link: %v", err) } rec := serveDownload(t, h, app.ID, "app.apk") if rec.Code != http.StatusFound { t.Fatalf("status = %d, want 302", rec.Code) } if loc := rec.Header().Get("Location"); loc != link { t.Fatalf("Location = %q, want %q", loc, link) } if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "*" { t.Fatalf("redirect ACAO = %q", got) } } func TestDownloadPrivacySwitchOff(t *testing.T) { t.Setenv("DCNTRLIZED_PUBLIC_PROFILE", "false") h, _ := newTestHandler(t) app := seedHostedApp(t, h, "com.example.private", "bytes") if rec := serveDownload(t, h, app.ID, app.Artifact.Filename); rec.Code != http.StatusNotFound { t.Fatalf("status = %d, want 404 when the profile switch is off", rec.Code) } req := httptest.NewRequest(http.MethodGet, "/api/catalog/icons/"+app.ID, nil) req.SetPathValue("id", app.ID) rec := httptest.NewRecorder() h.IconBytes(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("icon status = %d, want 404 when the profile switch is off", rec.Code) } } func TestIconBytes(t *testing.T) { h, _ := newTestHandler(t) app := seedHostedApp(t, h, "com.example.icon2", "bytes") icon := append([]byte("\x89PNG\r\n\x1a\n"), []byte("png-body")...) if _, err := h.store.SaveIcon(app.ID, icon); err != nil { t.Fatalf("save icon: %v", err) } stored, _ := h.store.Get(app.ID) stored.IconPath = "icons/" + app.ID if err := h.store.Upsert(*stored); err != nil { t.Fatalf("persist icon path: %v", err) } req := httptest.NewRequest(http.MethodGet, "/api/catalog/icons/"+app.ID, nil) req.SetPathValue("id", app.ID) rec := httptest.NewRecorder() h.IconBytes(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d body %s", rec.Code, rec.Body.String()) } if rec.Body.String() != string(icon) { t.Fatalf("icon body = %q", rec.Body.String()) } if got := rec.Header().Get("Content-Type"); got != "image/png" { t.Fatalf("Content-Type = %q", got) } if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "*" { t.Fatalf("ACAO = %q", got) } // App without an uploaded icon → 404. other := seedHostedApp(t, h, "com.example.noicon", "bytes") req = httptest.NewRequest(http.MethodGet, "/api/catalog/icons/"+other.ID, nil) req.SetPathValue("id", other.ID) rec = httptest.NewRecorder() h.IconBytes(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("missing icon status = %d", rec.Code) } }