package routes import ( "encoding/json" "errors" "io/fs" "kooltap-updateserver/internal/db" "kooltap-updateserver/internal/models" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) func newHandler(t *testing.T, releases ...models.Release) (*Handler, *gin.Engine) { t.Helper() gin.SetMode(gin.TestMode) database, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{ TranslateError: true, Logger: logger.Discard, }) if err != nil { t.Fatal(err) } if err := db.Migrate(database); err != nil { t.Fatal(err) } for i := range releases { if err := database.Create(&releases[i]).Error; err != nil { t.Fatal(err) } } h := &Handler{DB: database, UploadDir: t.TempDir(), TmpDir: t.TempDir()} r := gin.New() h.Register(r) return h, r } func do(r *gin.Engine, method, url string) (*httptest.ResponseRecorder, map[string]any) { req := httptest.NewRequest(method, url, nil) req.Header.Set("X-Admin-Token", "secret") w := httptest.NewRecorder() r.ServeHTTP(w, req) var body map[string]any _ = json.Unmarshal(w.Body.Bytes(), &body) return w, body } func TestCheckUpdate(t *testing.T) { _, r := newHandler(t, models.Release{Version: "1.2.0", VersionCode: 12, APKFile: "app-1.2.0.apk"}, models.Release{Version: "1.10.0", VersionCode: 110, APKFile: "app-1.10.0.apk", Mandatory: true}, ) cases := []struct { query string want bool }{ {"version=1.2.0", true}, {"version=1.10.0", false}, {"version=2.0.0", false}, {"version=1.2.0&code=12", true}, {"version=1.10.0&code=110", false}, {"version=garbage", true}, {"version=garbage&code=200", false}, } for _, tc := range cases { t.Run(tc.query, func(t *testing.T) { w, body := do(r, http.MethodGet, "/api/update?"+tc.query) if w.Code != http.StatusOK { t.Fatalf("status %d", w.Code) } if body["update"] != tc.want { t.Fatalf("update = %v, want %v", body["update"], tc.want) } if tc.want && body["download"] != "/downloads/app-1.10.0.apk" { t.Fatalf("download = %v", body["download"]) } }) } } func TestCheckUpdateNoReleases(t *testing.T) { _, r := newHandler(t) _, body := do(r, http.MethodGet, "/api/update?version=1.0.0") if body["update"] != false { t.Fatalf("update = %v", body["update"]) } if w, _ := do(r, http.MethodGet, "/download/latest"); w.Code != http.StatusNotFound { t.Fatalf("download latest status %d", w.Code) } } func TestListReleasesEmptyIsArray(t *testing.T) { _, r := newHandler(t) w, _ := do(r, http.MethodGet, "/api/releases") if w.Body.String() != "[]" { t.Fatalf("body = %s", w.Body.String()) } } func TestDeleteRelease(t *testing.T) { t.Setenv("ADMIN_TOKEN", "secret") h, r := newHandler(t, models.Release{Version: "1.0.0", VersionCode: 1, APKFile: "app-1.0.0.apk"}) apk := filepath.Join(h.UploadDir, "app-1.0.0.apk") if err := os.WriteFile(apk, []byte("x"), 0644); err != nil { t.Fatal(err) } if w, _ := do(r, http.MethodDelete, "/admin/releases/abc"); w.Code != http.StatusNotFound { t.Fatalf("bad id status %d", w.Code) } if w, _ := do(r, http.MethodDelete, "/admin/releases/1"); w.Code != http.StatusOK { t.Fatalf("delete status %d", w.Code) } if _, err := os.Stat(apk); !errors.Is(err, fs.ErrNotExist) { t.Fatalf("apk still exists: %v", err) } if w, _ := do(r, http.MethodDelete, "/admin/releases/1"); w.Code != http.StatusNotFound { t.Fatalf("second delete status %d", w.Code) } } func TestMoveFileNeverOverwrites(t *testing.T) { dir := t.TempDir() src := filepath.Join(dir, "src") dst := filepath.Join(dir, "dst") os.WriteFile(src, []byte("new"), 0644) os.WriteFile(dst, []byte("old"), 0644) if err := moveFile(src, dst); !errors.Is(err, fs.ErrExist) { t.Fatalf("err = %v, want ErrExist", err) } if b, _ := os.ReadFile(dst); string(b) != "old" { t.Fatalf("dst overwritten: %q", b) } os.Remove(dst) if err := moveFile(src, dst); err != nil { t.Fatal(err) } if b, _ := os.ReadFile(dst); string(b) != "new" { t.Fatalf("dst = %q", b) } } func TestSafeName(t *testing.T) { cases := map[string]string{ "1.2.3": "1.2.3", "1.0.0-beta+7": "1.0.0-beta+7", "../../etc/x": ".._.._etc_x", "..": "_", "": "_", `1.0\..\evil`: "1.0_.._evil", "1.0 (release)": "1.0__release_", } for in, want := range cases { if got := safeName(in); got != want { t.Errorf("safeName(%q) = %q, want %q", in, got, want) } } }