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): } }