feat(api): Android self-update endpoints proxied from Gitea releases (M5)
GET /api/v1/app/android/latest reads the latest Gitea release with the server token (cached for 10 minutes); the download endpoint caches the APK under /updates, verifies its SHA-256 and serves it with Range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9fd616da57
commit
6a67c27f50
10 files changed
+940
-173
No files matched your search
@@ -0,0 +1,77 @@
|
||||
package appupdate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVersionCode(t *testing.T) {
|
||||
cases := map[string]int{"v0.6.1": 60_199, "1.2.0-rc.3": 1_020_003, "v10.0.0": 10_000_099}
|
||||
for tag, want := range cases {
|
||||
_, got, err := VersionCode(tag)
|
||||
require.NoError(t, err, tag)
|
||||
assert.Equal(t, want, got, tag)
|
||||
}
|
||||
for _, bad := range []string{"latest", "v1.100.0", "1.2"} {
|
||||
_, _, err := VersionCode(bad)
|
||||
assert.Error(t, err, bad)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLatestAndDownload(t *testing.T) {
|
||||
apk := []byte("fake apk bytes")
|
||||
sum := sha256.Sum256(apk)
|
||||
var srv *httptest.Server
|
||||
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "token secret", r.Header.Get("Authorization"))
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/repos/o/r/releases/latest":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"tag_name": "v0.6.1", "body": "## Новое",
|
||||
"assets": []map[string]any{
|
||||
{"name": "pipu-music-0.6.1.apk", "size": len(apk), "browser_download_url": srv.URL + "/apk"},
|
||||
{"name": "pipu-music-0.6.1.apk.sha256", "size": 65, "browser_download_url": srv.URL + "/sum"},
|
||||
},
|
||||
})
|
||||
case "/apk":
|
||||
_, _ = w.Write(apk)
|
||||
case "/sum":
|
||||
_, _ = w.Write([]byte(hex.EncodeToString(sum[:]) + "\n"))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
dir := t.TempDir()
|
||||
h := NewHandler(Config{GiteaURL: srv.URL, Repo: "o/r", Token: "secret", Dir: dir, MinSupported: 60_000}, slog.Default())
|
||||
l, err := h.latest(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "0.6.1", l.Version)
|
||||
assert.Equal(t, 60_199, l.VersionCode)
|
||||
assert.Equal(t, hex.EncodeToString(sum[:]), l.SHA256)
|
||||
|
||||
path, err := h.ensure(context.Background(), l)
|
||||
require.NoError(t, err)
|
||||
got, _ := os.ReadFile(path)
|
||||
assert.Equal(t, apk, got)
|
||||
|
||||
// Подменённый файл не пройдёт сверку.
|
||||
require.NoError(t, os.Remove(path))
|
||||
l.SHA256 = "00" + l.SHA256[2:]
|
||||
_, err = h.ensure(context.Background(), l)
|
||||
assert.ErrorContains(t, err, "checksum mismatch")
|
||||
_, statErr := os.Stat(filepath.Join(dir, "pipu-music-0.6.1.apk"))
|
||||
assert.True(t, os.IsNotExist(statErr))
|
||||
}
|
||||
Reference in new issue
Block a user