316 lines
11 KiB
Go
316 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¤t_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¤t_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"))
|
|
stableDownload, err := http.Get(server.URL + "/api/v1/packages/channel-beta/1.0.0/download?platform=linux&architecture=amd64&channel=stable")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stableDownload.Body.Close()
|
|
if stableDownload.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("稳定通道不应下载 beta 包:%d", stableDownload.StatusCode)
|
|
}
|
|
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):
|
|
}
|
|
}
|