Scout/pkg/client/projects_test.go
2026-08-31 21:19:15 +08:00

155 lines
6.1 KiB
Go

package client
import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
)
func TestProjectClientUsesProjectScopedPaths(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.URL.Path != "/api/v1/projects/ssl/packages/demo" {
t.Fatalf("项目请求路径错误:%s", request.URL.Path)
}
writer.Header().Set("Content-Type", "application/json")
_, _ = writer.Write([]byte(`{"name":"demo","version":"1.0.0","project":"ssl"}`))
}))
defer server.Close()
api, err := NewProject(server.URL, "ssl")
if err != nil {
t.Fatal(err)
}
item, err := api.GetLatest(context.Background(), "demo")
if err != nil {
t.Fatal(err)
}
if item.Project != "ssl" || item.Version != "1.0.0" {
t.Fatalf("项目包响应错误:%+v", item)
}
}
func TestProjectClientUsesProjectScopedVersionPaths(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.URL.Path != "/api/v1/projects/ssl/packages/demo/versions" {
t.Fatalf("项目版本请求路径错误:%s", request.URL.Path)
}
writer.Header().Set("Content-Type", "application/json")
_, _ = writer.Write([]byte(`{"name":"demo","versions":[{"name":"demo","version":"1.0.0","project":"ssl"}]}`))
}))
defer server.Close()
api, err := NewProject(server.URL, "ssl")
if err != nil {
t.Fatal(err)
}
versions, err := api.ListVersionsFor(context.Background(), "demo", QueryOptions{})
if err != nil {
t.Fatal(err)
}
if len(versions) != 1 || versions[0].Project != "ssl" {
t.Fatalf("项目版本响应错误:%+v", versions)
}
}
func TestProjectDownloadKeepsDestinationWhenChecksumFails(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Length", "7")
writer.Header().Set("X-Scout-SHA256", "invalid")
_, _ = writer.Write([]byte("payload"))
}))
defer server.Close()
api, err := NewProject(server.URL, "ssl")
if err != nil {
t.Fatal(err)
}
destination := filepath.Join(t.TempDir(), "package.pkg")
if err := os.WriteFile(destination, []byte("original"), 0o600); err != nil {
t.Fatal(err)
}
if _, err := api.DownloadFor(context.Background(), "demo", "1.0.0", destination, QueryOptions{}); err == nil {
t.Fatal("错误校验值应该导致下载失败")
}
data, err := os.ReadFile(destination)
if err != nil {
t.Fatal(err)
}
if string(data) != "original" {
t.Fatalf("项目下载校验失败不应覆盖原文件:%q", data)
}
}
func TestNewProjectRejectsUnsafeProjectID(t *testing.T) {
if _, err := NewProject("http://127.0.0.1", "../ssl"); err == nil {
t.Fatal("不安全项目标识应该失败")
}
}
func TestProjectClientAccessMethodsUseProjectToken(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.Header.Get("Authorization") != "Bearer session-token" {
t.Fatalf("session authorization header missing: %q", request.Header.Get("Authorization"))
}
if request.Header.Get("X-Project-Token") != "sct_project-token" {
t.Fatalf("project token header missing: %q", request.Header.Get("X-Project-Token"))
}
writer.Header().Set("Content-Type", "application/json")
switch {
case request.Method == http.MethodGet && request.URL.Path == "/api/v1/projects/ssl/members":
_, _ = writer.Write([]byte(`{"members":[{"project_id":"ssl","user_id":"user-1","role":"project_viewer"}]}`))
case request.Method == http.MethodPost && request.URL.Path == "/api/v1/projects/ssl/members":
_, _ = writer.Write([]byte(`{"project_id":"ssl","user_id":"user-2","role":"project_developer"}`))
case request.Method == http.MethodPatch && request.URL.Path == "/api/v1/projects/ssl/members/user-2":
_, _ = writer.Write([]byte(`{"project_id":"ssl","user_id":"user-2","role":"project_admin"}`))
case request.Method == http.MethodDelete && request.URL.Path == "/api/v1/projects/ssl/members/user-2":
writer.WriteHeader(http.StatusNoContent)
return
case request.Method == http.MethodGet && request.URL.Path == "/api/v1/projects/ssl/tokens":
_, _ = writer.Write([]byte(`{"tokens":[{"id":"tok-1","project_id":"ssl","name":"client","role":"project_viewer"}]}`))
case request.Method == http.MethodPost && request.URL.Path == "/api/v1/projects/ssl/tokens":
_, _ = writer.Write([]byte(`{"token":{"id":"tok-2","project_id":"ssl","name":"writer","role":"project_developer"},"secret":"sct_created"}`))
case request.Method == http.MethodPost && request.URL.Path == "/api/v1/projects/ssl/tokens/tok-2/revoke":
_, _ = writer.Write([]byte(`{"id":"tok-2","project_id":"ssl","name":"writer","role":"project_developer","revoked_at":"2026-08-31T00:00:00Z"}`))
default:
http.NotFound(writer, request)
}
}))
defer server.Close()
api, err := NewProject(server.URL, "ssl", WithDeveloperToken("session-token"), WithProjectToken("sct_project-token"))
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
members, err := api.ListProjectMembers(ctx)
if err != nil || len(members) != 1 || members[0].UserID != "user-1" {
t.Fatalf("list members failed: %+v, %v", members, err)
}
member, err := api.AddProjectMember(ctx, "user-2", "project_developer")
if err != nil || member.UserID != "user-2" {
t.Fatalf("add member failed: %+v, %v", member, err)
}
member, err = api.UpdateProjectMember(ctx, "user-2", "project_admin")
if err != nil || member.Role != "project_admin" {
t.Fatalf("update member failed: %+v, %v", member, err)
}
if err := api.DeleteProjectMember(ctx, "user-2"); err != nil {
t.Fatalf("delete member failed: %v", err)
}
tokens, err := api.ListProjectTokens(ctx)
if err != nil || len(tokens) != 1 || tokens[0].ID != "tok-1" {
t.Fatalf("list tokens failed: %+v, %v", tokens, err)
}
created, err := api.CreateProjectToken(ctx, "writer", "project_developer", nil)
if err != nil || created.Secret != "sct_created" || created.Token.ID != "tok-2" {
t.Fatalf("create token failed: %+v, %v", created, err)
}
revoked, err := api.RevokeProjectToken(ctx, "tok-2")
if err != nil || revoked.RevokedAt == nil {
t.Fatalf("revoke token failed: %+v, %v", revoked, err)
}
}