Scout/internal/httpapi/targets_test.go
2026-08-31 15:34:36 +08:00

308 lines
11 KiB
Go

package httpapi
import (
"archive/zip"
"bufio"
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"scout/internal/registry"
"scout/pkg/scoutpkg"
)
func TestScoutUploadRegistersTargetsAndUsesArchiveDigest(t *testing.T) {
store, err := registry.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer((&Server{Registry: store, DeveloperToken: "test-token"}).Handler())
defer server.Close()
archive := buildScoutArchive(t, scoutpkg.Manifest{
SchemaVersion: 1,
Name: "scout-multi",
Version: "2.0.0",
Channel: "stable",
PackageType: "full",
Targets: []scoutpkg.Target{
{Platform: "linux", Architecture: "amd64", Path: "targets/linux/amd64/app"},
{Platform: "windows", Architecture: "amd64", Path: "targets/windows/amd64/app.exe"},
},
}, map[string]string{
"targets/linux/amd64/app": "linux-binary",
"targets/windows/amd64/app.exe": "windows-binary",
})
response := uploadTargetPackage(t, server.URL, "test-token", map[string]string{}, "multi.scout", archive)
defer response.Body.Close()
if response.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(response.Body)
t.Fatalf("Scout \u4e0a\u4f20\u72b6\u6001\u9519\u8bef: %d %s", response.StatusCode, body)
}
var uploaded registry.Package
if err := json.NewDecoder(response.Body).Decode(&uploaded); err != nil {
t.Fatal(err)
}
if len(uploaded.Targets) != 2 {
t.Fatalf("Scout \u76ee\u6807\u6570\u91cf\u9519\u8bef: %d", len(uploaded.Targets))
}
digestValue := sha256.Sum256(archive)
digest := hex.EncodeToString(digestValue[:])
artifactID := uploaded.Targets[0].ArtifactID
for _, target := range uploaded.Targets {
if target.SHA256 != digest {
t.Fatalf("Scout \u4ea7\u7269\u6821\u9a8c\u503c\u4e0d\u662f\u6574\u4e2a\u6587\u4ef6\u54c8\u5e0c: %s", target.SHA256)
}
if target.ArtifactID != artifactID {
t.Fatalf("Scout \u591a\u76ee\u6807\u672a\u5171\u7528\u4ea7\u7269: %s != %s", target.ArtifactID, artifactID)
}
}
for _, platform := range []string{"linux", "windows"} {
download := getTargetPackage(t, server.URL+"/api/v1/packages/scout-multi/2.0.0/download?platform="+platform+"&architecture=amd64")
content, err := io.ReadAll(download.Body)
download.Body.Close()
if err != nil {
t.Fatal(err)
}
if download.StatusCode != http.StatusOK || !bytes.Equal(content, archive) {
t.Fatalf("%s Scout \u4e0b\u8f7d\u5185\u5bb9\u9519\u8bef: %d", platform, download.StatusCode)
}
if download.Header.Get("X-Scout-SHA256") != digest {
t.Fatalf("%s Scout \u4e0b\u8f7d\u6821\u9a8c\u503c\u9519\u8bef", platform)
}
if download.Header.Get("X-Scout-Platform") != platform {
t.Fatalf("%s Scout \u4e0b\u8f7d\u5e73\u53f0\u54cd\u5e94\u5934\u9519\u8bef", platform)
}
}
}
func TestTargetDownloadChannelAndDeltaSelection(t *testing.T) {
store, err := registry.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer((&Server{Registry: store, DeveloperToken: "test-token"}).Handler())
defer server.Close()
uploadTargetPackageOK(t, server.URL, map[string]string{
"name": "native-demo", "version": "1.0.0", "platform": "linux", "architecture": "amd64",
}, "linux.pkg", []byte("linux-content"))
uploadTargetPackageOK(t, server.URL, map[string]string{
"name": "native-demo", "version": "1.0.0", "platform": "windows", "architecture": "amd64",
}, "windows.pkg", []byte("windows-content"))
assertDownloadContent(t, server.URL+"/api/v1/packages/native-demo/1.0.0/download?platform=linux&architecture=amd64", "linux-content", "linux", "full")
assertDownloadContent(t, server.URL+"/api/v1/packages/native-demo/1.0.0/download?platform=windows&architecture=amd64", "windows-content", "windows", "full")
uploadTargetPackageOK(t, server.URL, map[string]string{
"name": "delta-demo", "version": "2.0.0", "platform": "linux", "architecture": "amd64", "package_type": "full",
}, "full.pkg", []byte("full-content"))
uploadTargetPackageOK(t, server.URL, map[string]string{
"name": "delta-demo", "version": "2.0.0", "platform": "linux", "architecture": "amd64", "package_type": "delta", "upgrade_from": "1.0.0",
}, "delta.pkg", []byte("delta-content"))
assertDownloadContent(t, server.URL+"/api/v1/packages/delta-demo/2.0.0/download?platform=linux&architecture=amd64&current_version=1.0.0", "delta-content", "linux", "delta")
assertDownloadContent(t, server.URL+"/api/v1/packages/delta-demo/2.0.0/download?platform=linux&architecture=amd64&current_version=1.5.0", "full-content", "linux", "full")
uploadTargetPackageOK(t, server.URL, map[string]string{
"name": "channel-stable", "version": "1.0.0", "platform": "linux", "architecture": "amd64", "channel": "stable",
}, "stable.pkg", []byte("stable"))
uploadTargetPackageOK(t, server.URL, map[string]string{
"name": "channel-beta", "version": "1.0.0", "platform": "linux", "architecture": "amd64", "channel": "beta",
}, "beta.pkg", []byte("beta"))
list := getTargetPackage(t, server.URL+"/api/v1/packages?platform=linux&architecture=amd64&channel=stable")
defer list.Body.Close()
var payload struct {
Packages []registry.Package `json:"packages"`
}
if err := json.NewDecoder(list.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
for _, item := range payload.Packages {
if item.Channel != "stable" || item.Name == "channel-beta" {
t.Fatalf("stable \u67e5\u8be2\u8fd4\u56de\u4e86\u975e stable \u5305: %+v", item)
}
}
}
func TestStreamFiltersPlatformVersionAndChannel(t *testing.T) {
store, err := registry.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer((&Server{Registry: store}).Handler())
defer server.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL+"/api/v1/updates/stream?packages=notify-demo&platform=linux&architecture=amd64&channel=stable&versions=notify-demo:2.0.0", nil)
if err != nil {
t.Fatal(err)
}
response, err := http.DefaultClient.Do(request)
if err != nil {
t.Fatal(err)
}
defer response.Body.Close()
updates := make(chan registry.Update, 4)
go scanUpdateEvents(response.Body, updates)
addNotificationArtifact(t, store, "2.1.0", "windows", "stable")
assertNoUpdate(t, updates, "Windows \u66f4\u65b0\u4e0d\u5e94\u901a\u77e5 Linux \u5ba2\u6237\u7aef")
addNotificationArtifact(t, store, "1.9.0", "linux", "stable")
assertNoUpdate(t, updates, "\u4f4e\u4e8e\u5f53\u524d\u7248\u672c\u7684\u66f4\u65b0\u4e0d\u5e94\u901a\u77e5\u5ba2\u6237\u7aef")
addNotificationArtifact(t, store, "2.2.0", "linux", "beta")
assertNoUpdate(t, updates, "beta \u66f4\u65b0\u4e0d\u5e94\u901a\u77e5 stable \u5ba2\u6237\u7aef")
addNotificationArtifact(t, store, "2.3.0", "linux", "stable")
select {
case update := <-updates:
if update.Package.Version != "2.3.0" || update.Package.Platform != "linux" || update.Package.Architecture != "amd64" {
t.Fatalf("\u7cbe\u51c6\u901a\u77e5\u5185\u5bb9\u9519\u8bef: %+v", update.Package)
}
case <-time.After(2 * time.Second):
t.Fatal("\u672a\u6536\u5230\u5339\u914d\u5e73\u53f0\u3001\u7248\u672c\u548c\u901a\u9053\u7684\u66f4\u65b0\u901a\u77e5")
}
}
func buildScoutArchive(t *testing.T, manifest scoutpkg.Manifest, files map[string]string) []byte {
t.Helper()
buffer := bytes.NewBuffer(nil)
writer := zip.NewWriter(buffer)
manifestData, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
entry, err := writer.Create(scoutpkg.ManifestName)
if err != nil {
t.Fatal(err)
}
if _, err := entry.Write(manifestData); err != nil {
t.Fatal(err)
}
for name, content := range files {
entry, err := writer.Create(name)
if err != nil {
t.Fatal(err)
}
if _, err := entry.Write([]byte(content)); err != nil {
t.Fatal(err)
}
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
return buffer.Bytes()
}
func uploadTargetPackage(t *testing.T, baseURL, token string, fields map[string]string, fileName string, content []byte) *http.Response {
t.Helper()
body := bytes.NewBuffer(nil)
writer := multipart.NewWriter(body)
for name, value := range fields {
if err := writer.WriteField(name, value); err != nil {
t.Fatal(err)
}
}
file, err := writer.CreateFormFile("file", fileName)
if err != nil {
t.Fatal(err)
}
if _, err := file.Write(content); err != nil {
t.Fatal(err)
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
request, err := http.NewRequest(http.MethodPost, baseURL+"/api/v1/packages/upload", body)
if err != nil {
t.Fatal(err)
}
request.Header.Set("Content-Type", writer.FormDataContentType())
request.Header.Set("Authorization", "Bearer "+token)
response, err := http.DefaultClient.Do(request)
if err != nil {
t.Fatal(err)
}
return response
}
func uploadTargetPackageOK(t *testing.T, baseURL string, fields map[string]string, fileName string, content []byte) {
t.Helper()
response := uploadTargetPackage(t, baseURL, "test-token", fields, fileName, content)
defer response.Body.Close()
if response.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(response.Body)
t.Fatalf("\u4e0a\u4f20\u72b6\u6001\u9519\u8bef: %d %s", response.StatusCode, body)
}
}
func getTargetPackage(t *testing.T, target string) *http.Response {
t.Helper()
response, err := http.Get(target)
if err != nil {
t.Fatal(err)
}
return response
}
func assertDownloadContent(t *testing.T, target, expectedContent, expectedPlatform, expectedType string) {
t.Helper()
response := getTargetPackage(t, target)
content, err := io.ReadAll(response.Body)
response.Body.Close()
if err != nil {
t.Fatal(err)
}
if response.StatusCode != http.StatusOK || string(content) != expectedContent {
t.Fatalf("\u4e0b\u8f7d\u5185\u5bb9\u9519\u8bef: %d %q", response.StatusCode, content)
}
if response.Header.Get("X-Scout-Platform") != expectedPlatform || response.Header.Get("X-Scout-Package-Type") != expectedType {
t.Fatalf("\u4e0b\u8f7d\u76ee\u6807\u54cd\u5e94\u5934\u9519\u8bef: %s %s", response.Header.Get("X-Scout-Platform"), response.Header.Get("X-Scout-Package-Type"))
}
}
func scanUpdateEvents(body io.Reader, updates chan<- registry.Update) {
scanner := bufio.NewScanner(body)
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "data: ") {
continue
}
var update registry.Update
if json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &update) == nil {
updates <- update
}
}
}
func addNotificationArtifact(t *testing.T, store *registry.Registry, version, platform, channel string) {
t.Helper()
_, err := store.AddArtifact(registry.Package{
Name: "notify-demo", Version: version, Channel: channel, Status: "published",
}, registry.Target{
Platform: platform, Architecture: "amd64", PackageType: "full", FileName: platform + "-" + version + ".pkg",
}, bytes.NewBufferString(platform+"-"+version))
if err != nil {
t.Fatal(err)
}
}
func assertNoUpdate(t *testing.T, updates <-chan registry.Update, message string) {
t.Helper()
select {
case update := <-updates:
t.Fatalf("%s: %+v", message, update.Package)
case <-time.After(120 * time.Millisecond):
}
}