Refactor GitHub workflows for Registry Client SDK tests
- Add environment variables for registry initialization in both `pr-test.yml` and `unit-test.yml`. - Implement a waiting mechanism for the registry service to ensure readiness before executing tests. - Remove redundant steps for user creation and service restart, streamlining the test setup process. - Enhance the client code to support auto-discovery of the API prefix, improving flexibility in server URL configuration. - Introduce comprehensive error handling in client tests to cover various edge cases and improve test coverage.
This commit is contained in:
parent
691802ae62
commit
35ab1d1040
5 changed files with 430 additions and 69 deletions
31
.github/workflows/pr-test.yml
vendored
31
.github/workflows/pr-test.yml
vendored
|
|
@ -1378,6 +1378,9 @@ jobs:
|
|||
image: yaoapp/registry:latest
|
||||
ports:
|
||||
- "8080:8080"
|
||||
env:
|
||||
REGISTRY_INIT_USER: yaoagents
|
||||
REGISTRY_INIT_PASS: yaoagents
|
||||
strategy:
|
||||
matrix:
|
||||
go: ["1.25"]
|
||||
|
|
@ -1428,6 +1431,17 @@ jobs:
|
|||
body: '🤖 Registry Client SDK Tests running...'
|
||||
});
|
||||
|
||||
- name: Wait for Registry
|
||||
run: |
|
||||
for i in $(seq 1 15); do
|
||||
if curl -sf http://localhost:8080/.well-known/yao-registry > /dev/null 2>&1; then
|
||||
echo "Registry is ready"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for registry... ($i)"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Checkout Kun
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
|
|
@ -1493,23 +1507,6 @@ jobs:
|
|||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Create Registry Test User
|
||||
run: |
|
||||
docker exec ${{ job.services.yao-registry.id }} \
|
||||
registry user add --password yaoagents yaoagents
|
||||
|
||||
- name: Restart Registry Service
|
||||
run: |
|
||||
docker restart ${{ job.services.yao-registry.id }}
|
||||
for i in $(seq 1 15); do
|
||||
if curl -sf http://localhost:8080/.well-known/yao-registry > /dev/null 2>&1; then
|
||||
echo "Registry is ready"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for registry... ($i)"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run Registry Client Tests
|
||||
env:
|
||||
YAO_REGISTRY_URL: http://localhost:8080
|
||||
|
|
|
|||
31
.github/workflows/unit-test.yml
vendored
31
.github/workflows/unit-test.yml
vendored
|
|
@ -1042,10 +1042,24 @@ jobs:
|
|||
image: yaoapp/registry:latest
|
||||
ports:
|
||||
- "8080:8080"
|
||||
env:
|
||||
REGISTRY_INIT_USER: yaoagents
|
||||
REGISTRY_INIT_PASS: yaoagents
|
||||
strategy:
|
||||
matrix:
|
||||
go: ["1.25"]
|
||||
steps:
|
||||
- name: Wait for Registry
|
||||
run: |
|
||||
for i in $(seq 1 15); do
|
||||
if curl -sf http://localhost:8080/.well-known/yao-registry > /dev/null 2>&1; then
|
||||
echo "Registry is ready"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for registry... ($i)"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Checkout Kun
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
|
|
@ -1109,23 +1123,6 @@ jobs:
|
|||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Create Registry Test User
|
||||
run: |
|
||||
docker exec ${{ job.services.yao-registry.id }} \
|
||||
registry user add --password yaoagents yaoagents
|
||||
|
||||
- name: Restart Registry Service
|
||||
run: |
|
||||
docker restart ${{ job.services.yao-registry.id }}
|
||||
for i in $(seq 1 15); do
|
||||
if curl -sf http://localhost:8080/.well-known/yao-registry > /dev/null 2>&1; then
|
||||
echo "Registry is ready"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for registry... ($i)"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run Registry Client Tests
|
||||
env:
|
||||
YAO_REGISTRY_URL: http://localhost:8080
|
||||
|
|
|
|||
56
registry/README.md
Normal file
56
registry/README.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# registry
|
||||
|
||||
Go client SDK for [Yao Registry](https://github.com/YaoApp/registry).
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
import "github.com/yaoapp/yao/registry"
|
||||
|
||||
// Only the server URL is required. API prefix is auto-discovered
|
||||
// via /.well-known/yao-registry on the first call.
|
||||
c := registry.New("https://registry.yaoagents.com",
|
||||
registry.WithAuth("user", "pass"), // optional, for push/delete
|
||||
)
|
||||
|
||||
// Push a .yao.zip package
|
||||
result, err := c.Push("assistants", "@yao", "hello", "1.0.0", zipBytes)
|
||||
|
||||
// Pull (by version or dist-tag)
|
||||
data, digest, err := c.Pull("assistants", "@yao", "hello", "latest")
|
||||
|
||||
// Query
|
||||
pack, err := c.GetPackument("assistants", "@yao", "hello")
|
||||
ver, err := c.GetVersion("assistants", "@yao", "hello", "1.0.0")
|
||||
list, err := c.Search("hello", "assistants", 1, 20)
|
||||
|
||||
// Dependencies
|
||||
deps, err := c.GetDependencies("assistants", "@yao", "hello", "1.0.0", true)
|
||||
|
||||
// Dist-tags
|
||||
c.SetTag("assistants", "@yao", "hello", "stable", "1.0.0")
|
||||
c.DeleteTag("assistants", "@yao", "hello", "stable")
|
||||
|
||||
// Delete
|
||||
c.DeleteVersion("assistants", "@yao", "hello", "1.0.0")
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `WithAuth(user, pass)` | Basic Auth for push/delete |
|
||||
| `WithHTTPClient(hc)` | Custom `*http.Client` |
|
||||
| `WithTimeout(d)` | HTTP timeout |
|
||||
|
||||
## Environment
|
||||
|
||||
Tests require a running registry server. Set `YAO_REGISTRY_URL` (default `http://localhost:8080`) and create user `yaoagents`/`yaoagents`.
|
||||
|
||||
```bash
|
||||
# Start registry with test user
|
||||
REGISTRY_INIT_USER=yaoagents REGISTRY_INIT_PASS=yaoagents registry start
|
||||
|
||||
# Run tests
|
||||
go test ./registry/... -v
|
||||
```
|
||||
|
|
@ -11,15 +11,20 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client talks to a Yao Registry server over HTTP.
|
||||
// On first API call it discovers the API prefix via /.well-known/yao-registry
|
||||
// so callers only need to provide the server root URL (e.g. "https://registry.yaoagents.com").
|
||||
type Client struct {
|
||||
baseURL string
|
||||
username string
|
||||
password string
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
apiPrefix string // resolved from well-known, e.g. "/v1"
|
||||
discoverOnce sync.Once
|
||||
username string
|
||||
password string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// Option configures a Client.
|
||||
|
|
@ -43,10 +48,13 @@ func WithTimeout(d time.Duration) Option {
|
|||
return func(c *Client) { c.httpClient.Timeout = d }
|
||||
}
|
||||
|
||||
// New creates a registry client. serverURL is the base URL without trailing slash.
|
||||
// New creates a registry client. serverURL is the root URL users configure,
|
||||
// e.g. "http://localhost:8080" or "https://registry.yaoagents.com".
|
||||
// The actual API prefix is auto-discovered via /.well-known/yao-registry.
|
||||
func New(serverURL string, opts ...Option) *Client {
|
||||
c := &Client{
|
||||
baseURL: strings.TrimRight(serverURL, "/"),
|
||||
apiPrefix: "/v1", // sensible default, overridden by discovery
|
||||
httpClient: &http.Client{Timeout: 60 * time.Second},
|
||||
}
|
||||
for _, o := range opts {
|
||||
|
|
@ -55,6 +63,16 @@ func New(serverURL string, opts ...Option) *Client {
|
|||
return c
|
||||
}
|
||||
|
||||
// ensureDiscovered runs well-known discovery exactly once (thread-safe).
|
||||
func (c *Client) ensureDiscovered() {
|
||||
c.discoverOnce.Do(func() {
|
||||
var info RegistryInfo
|
||||
if err := c.doGet("/.well-known/yao-registry", nil, &info); err == nil && info.Registry.API != "" {
|
||||
c.apiPrefix = strings.TrimRight(info.Registry.API, "/")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- Response types ---
|
||||
|
||||
// RegistryInfo is returned by the discovery endpoint.
|
||||
|
|
@ -184,16 +202,17 @@ func (e *APIError) Error() string {
|
|||
// Discover calls GET /.well-known/yao-registry.
|
||||
func (c *Client) Discover() (*RegistryInfo, error) {
|
||||
var info RegistryInfo
|
||||
if err := c.get("/.well-known/yao-registry", nil, &info); err != nil {
|
||||
if err := c.doGet("/.well-known/yao-registry", nil, &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// Info calls GET /v1/.
|
||||
// Info calls GET {apiPrefix}/.
|
||||
func (c *Client) Info() (*ServerInfo, error) {
|
||||
c.ensureDiscovered()
|
||||
var info ServerInfo
|
||||
if err := c.get("/v1/", nil, &info); err != nil {
|
||||
if err := c.doGet(c.apiPrefix+"/", nil, &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &info, nil
|
||||
|
|
@ -201,8 +220,9 @@ func (c *Client) Info() (*ServerInfo, error) {
|
|||
|
||||
// --- List & Search ---
|
||||
|
||||
// List calls GET /v1/:type with optional filters.
|
||||
// List calls GET {apiPrefix}/:type with optional filters.
|
||||
func (c *Client) List(pkgType string, scope string, query string, page, pageSize int) (*ListResult, error) {
|
||||
c.ensureDiscovered()
|
||||
params := url.Values{}
|
||||
if scope != "" {
|
||||
params.Set("scope", scope)
|
||||
|
|
@ -217,14 +237,15 @@ func (c *Client) List(pkgType string, scope string, query string, page, pageSize
|
|||
params.Set("pagesize", fmt.Sprintf("%d", pageSize))
|
||||
}
|
||||
var result ListResult
|
||||
if err := c.get("/v1/"+pkgType, params, &result); err != nil {
|
||||
if err := c.doGet(c.apiPrefix+"/"+pkgType, params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// Search calls GET /v1/search.
|
||||
// Search calls GET {apiPrefix}/search.
|
||||
func (c *Client) Search(q string, pkgType string, page, pageSize int) (*ListResult, error) {
|
||||
c.ensureDiscovered()
|
||||
params := url.Values{"q": {q}}
|
||||
if pkgType != "" {
|
||||
params.Set("type", pkgType)
|
||||
|
|
@ -236,7 +257,7 @@ func (c *Client) Search(q string, pkgType string, page, pageSize int) (*ListResu
|
|||
params.Set("pagesize", fmt.Sprintf("%d", pageSize))
|
||||
}
|
||||
var result ListResult
|
||||
if err := c.get("/v1/search", params, &result); err != nil {
|
||||
if err := c.doGet(c.apiPrefix+"/search", params, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
|
|
@ -244,21 +265,23 @@ func (c *Client) Search(q string, pkgType string, page, pageSize int) (*ListResu
|
|||
|
||||
// --- Package metadata ---
|
||||
|
||||
// GetPackument calls GET /v1/:type/:scope/:name.
|
||||
// GetPackument calls GET {apiPrefix}/:type/:scope/:name.
|
||||
func (c *Client) GetPackument(pkgType, scope, name string) (*Packument, error) {
|
||||
c.ensureDiscovered()
|
||||
var p Packument
|
||||
path := fmt.Sprintf("/v1/%s/%s/%s", pkgType, scope, name)
|
||||
if err := c.get(path, nil, &p); err != nil {
|
||||
path := fmt.Sprintf("%s/%s/%s/%s", c.apiPrefix, pkgType, scope, name)
|
||||
if err := c.doGet(path, nil, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// GetVersion calls GET /v1/:type/:scope/:name/:version.
|
||||
// GetVersion calls GET {apiPrefix}/:type/:scope/:name/:version.
|
||||
func (c *Client) GetVersion(pkgType, scope, name, version string) (*VersionDetail, error) {
|
||||
c.ensureDiscovered()
|
||||
var v VersionDetail
|
||||
path := fmt.Sprintf("/v1/%s/%s/%s/%s", pkgType, scope, name, version)
|
||||
if err := c.get(path, nil, &v); err != nil {
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/%s", c.apiPrefix, pkgType, scope, name, version)
|
||||
if err := c.doGet(path, nil, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
|
|
@ -266,25 +289,27 @@ func (c *Client) GetVersion(pkgType, scope, name, version string) (*VersionDetai
|
|||
|
||||
// --- Dependencies ---
|
||||
|
||||
// GetDependencies calls GET /v1/:type/:scope/:name/:version/dependencies.
|
||||
// GetDependencies calls GET {apiPrefix}/:type/:scope/:name/:version/dependencies.
|
||||
func (c *Client) GetDependencies(pkgType, scope, name, version string, recursive bool) (*DependencyList, error) {
|
||||
path := fmt.Sprintf("/v1/%s/%s/%s/%s/dependencies", pkgType, scope, name, version)
|
||||
c.ensureDiscovered()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/%s/dependencies", c.apiPrefix, pkgType, scope, name, version)
|
||||
params := url.Values{}
|
||||
if recursive {
|
||||
params.Set("recursive", "true")
|
||||
}
|
||||
var dl DependencyList
|
||||
if err := c.get(path, params, &dl); err != nil {
|
||||
if err := c.doGet(path, params, &dl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dl, nil
|
||||
}
|
||||
|
||||
// GetDependents calls GET /v1/:type/:scope/:name/dependents.
|
||||
// GetDependents calls GET {apiPrefix}/:type/:scope/:name/dependents.
|
||||
func (c *Client) GetDependents(pkgType, scope, name string) (*DependentList, error) {
|
||||
path := fmt.Sprintf("/v1/%s/%s/%s/dependents", pkgType, scope, name)
|
||||
c.ensureDiscovered()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/dependents", c.apiPrefix, pkgType, scope, name)
|
||||
var dl DependentList
|
||||
if err := c.get(path, nil, &dl); err != nil {
|
||||
if err := c.doGet(path, nil, &dl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dl, nil
|
||||
|
|
@ -292,9 +317,10 @@ func (c *Client) GetDependents(pkgType, scope, name string) (*DependentList, err
|
|||
|
||||
// --- Push & Pull ---
|
||||
|
||||
// Push uploads a .yao.zip package via PUT /v1/:type/:scope/:name/:version.
|
||||
// Push uploads a .yao.zip package via PUT {apiPrefix}/:type/:scope/:name/:version.
|
||||
func (c *Client) Push(pkgType, scope, name, version string, zipData []byte) (*PushResult, error) {
|
||||
path := fmt.Sprintf("/v1/%s/%s/%s/%s", pkgType, scope, name, version)
|
||||
c.ensureDiscovered()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/%s", c.apiPrefix, pkgType, scope, name, version)
|
||||
req, err := http.NewRequest(http.MethodPut, c.baseURL+path, bytes.NewReader(zipData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -319,10 +345,11 @@ func (c *Client) Push(pkgType, scope, name, version string, zipData []byte) (*Pu
|
|||
return &result, nil
|
||||
}
|
||||
|
||||
// Pull downloads a .yao.zip via GET /v1/:type/:scope/:name/:version/pull.
|
||||
// Pull downloads a .yao.zip via GET {apiPrefix}/:type/:scope/:name/:version/pull.
|
||||
// The version parameter can be a semver or a dist-tag name.
|
||||
func (c *Client) Pull(pkgType, scope, name, version string) ([]byte, string, error) {
|
||||
path := fmt.Sprintf("/v1/%s/%s/%s/%s/pull", pkgType, scope, name, version)
|
||||
c.ensureDiscovered()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/%s/pull", c.apiPrefix, pkgType, scope, name, version)
|
||||
resp, err := c.httpClient.Get(c.baseURL + path)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
|
|
@ -343,9 +370,10 @@ func (c *Client) Pull(pkgType, scope, name, version string) ([]byte, string, err
|
|||
|
||||
// --- Tags ---
|
||||
|
||||
// SetTag calls PUT /v1/:type/:scope/:name/tags/:tag.
|
||||
// SetTag calls PUT {apiPrefix}/:type/:scope/:name/tags/:tag.
|
||||
func (c *Client) SetTag(pkgType, scope, name, tag, version string) (*TagResult, error) {
|
||||
path := fmt.Sprintf("/v1/%s/%s/%s/tags/%s", pkgType, scope, name, tag)
|
||||
c.ensureDiscovered()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/tags/%s", c.apiPrefix, pkgType, scope, name, tag)
|
||||
body, _ := json.Marshal(map[string]string{"version": version})
|
||||
|
||||
req, err := http.NewRequest(http.MethodPut, c.baseURL+path, bytes.NewReader(body))
|
||||
|
|
@ -372,9 +400,10 @@ func (c *Client) SetTag(pkgType, scope, name, tag, version string) (*TagResult,
|
|||
return &result, nil
|
||||
}
|
||||
|
||||
// DeleteTag calls DELETE /v1/:type/:scope/:name/tags/:tag.
|
||||
// DeleteTag calls DELETE {apiPrefix}/:type/:scope/:name/tags/:tag.
|
||||
func (c *Client) DeleteTag(pkgType, scope, name, tag string) (*TagDeleteResult, error) {
|
||||
path := fmt.Sprintf("/v1/%s/%s/%s/tags/%s", pkgType, scope, name, tag)
|
||||
c.ensureDiscovered()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/tags/%s", c.apiPrefix, pkgType, scope, name, tag)
|
||||
req, err := http.NewRequest(http.MethodDelete, c.baseURL+path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -400,9 +429,10 @@ func (c *Client) DeleteTag(pkgType, scope, name, tag string) (*TagDeleteResult,
|
|||
|
||||
// --- Delete ---
|
||||
|
||||
// DeleteVersion calls DELETE /v1/:type/:scope/:name/:version.
|
||||
// DeleteVersion calls DELETE {apiPrefix}/:type/:scope/:name/:version.
|
||||
func (c *Client) DeleteVersion(pkgType, scope, name, version string) (*DeleteResult, error) {
|
||||
path := fmt.Sprintf("/v1/%s/%s/%s/%s", pkgType, scope, name, version)
|
||||
c.ensureDiscovered()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/%s", c.apiPrefix, pkgType, scope, name, version)
|
||||
req, err := http.NewRequest(http.MethodDelete, c.baseURL+path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -434,7 +464,7 @@ func (c *Client) setAuth(req *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *Client) get(path string, params url.Values, out interface{}) error {
|
||||
func (c *Client) doGet(path string, params url.Values, out interface{}) error {
|
||||
u := c.baseURL + path
|
||||
if len(params) > 0 {
|
||||
u += "?" + params.Encode()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package registry_test
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -705,6 +706,88 @@ func TestDeleteNonExistentTag(t *testing.T) {
|
|||
c.DeleteVersion("assistants", testScope, name, "1.0.0")
|
||||
}
|
||||
|
||||
// --- Push / Delete error responses ---
|
||||
|
||||
func TestPushDuplicateVersion(t *testing.T) {
|
||||
c := newClient()
|
||||
zipData, _ := testdata.BuildZip(&testdata.Manifest{
|
||||
Type: "assistant", Scope: testScope, Name: "dup-push", Version: "1.0.0",
|
||||
}, nil)
|
||||
|
||||
defer cleanup(c, "assistants", testScope, "dup-push", "1.0.0")
|
||||
|
||||
_, err := c.Push("assistants", testScope, "dup-push", "1.0.0", zipData)
|
||||
if err != nil {
|
||||
t.Fatalf("first push failed: %v", err)
|
||||
}
|
||||
|
||||
_, err = c.Push("assistants", testScope, "dup-push", "1.0.0", zipData)
|
||||
if err == nil {
|
||||
t.Fatal("expected error on duplicate push")
|
||||
}
|
||||
apiErr, ok := err.(*registry.APIError)
|
||||
if !ok {
|
||||
t.Fatalf("expected APIError, got %T", err)
|
||||
}
|
||||
if apiErr.StatusCode != 409 {
|
||||
t.Errorf("expected 409, got %d", apiErr.StatusCode)
|
||||
}
|
||||
|
||||
c.DeleteVersion("assistants", testScope, "dup-push", "1.0.0")
|
||||
}
|
||||
|
||||
func TestDeleteNonExistentVersion(t *testing.T) {
|
||||
c := newClient()
|
||||
_, err := c.DeleteVersion("assistants", testScope, "never-existed", "9.9.9")
|
||||
if err == nil {
|
||||
t.Fatal("expected error deleting non-existent version")
|
||||
}
|
||||
apiErr, ok := err.(*registry.APIError)
|
||||
if !ok {
|
||||
t.Fatalf("expected APIError, got %T", err)
|
||||
}
|
||||
if apiErr.StatusCode != 404 {
|
||||
t.Errorf("expected 404, got %d", apiErr.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetTagNonExistentPackage(t *testing.T) {
|
||||
c := newClient()
|
||||
_, err := c.SetTag("assistants", testScope, "no-such-pkg", "beta", "1.0.0")
|
||||
if err == nil {
|
||||
t.Fatal("expected error setting tag on non-existent package")
|
||||
}
|
||||
apiErr, ok := err.(*registry.APIError)
|
||||
if !ok {
|
||||
t.Fatalf("expected APIError, got %T", err)
|
||||
}
|
||||
if apiErr.StatusCode != 404 {
|
||||
t.Errorf("expected 404, got %d", apiErr.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListWithQueryFilter(t *testing.T) {
|
||||
c := newClient()
|
||||
zipData, _ := testdata.BuildZip(&testdata.Manifest{
|
||||
Type: "mcp", Scope: testScope, Name: "list-query-mcp", Version: "1.0.0",
|
||||
Description: "filterable mcp tool",
|
||||
}, nil)
|
||||
|
||||
defer cleanup(c, "mcps", testScope, "list-query-mcp", "1.0.0")
|
||||
|
||||
c.Push("mcps", testScope, "list-query-mcp", "1.0.0", zipData)
|
||||
|
||||
list, err := c.List("mcps", testScope, "filterable", 1, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("List with scope+query failed: %v", err)
|
||||
}
|
||||
if list.Total < 1 {
|
||||
t.Errorf("expected at least 1 result, got %d", list.Total)
|
||||
}
|
||||
|
||||
c.DeleteVersion("mcps", testScope, "list-query-mcp", "1.0.0")
|
||||
}
|
||||
|
||||
// --- Release type CRUD ---
|
||||
|
||||
func TestReleaseCRUD(t *testing.T) {
|
||||
|
|
@ -759,3 +842,201 @@ func TestReleaseCRUD(t *testing.T) {
|
|||
t.Fatalf("Delete failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- httptest-based edge-case coverage ---
|
||||
|
||||
func TestParseErrorNonJSON(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("plain text error"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := registry.New(srv.URL, registry.WithAuth("u", "p"))
|
||||
_, err := c.Push("assistants", "@t", "x", "1.0.0", []byte("data"))
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
apiErr, ok := err.(*registry.APIError)
|
||||
if !ok {
|
||||
t.Fatalf("expected APIError, got %T", err)
|
||||
}
|
||||
if apiErr.Message != "plain text error" {
|
||||
t.Errorf("expected plain text body in message, got %q", apiErr.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidBaseURL(t *testing.T) {
|
||||
c := registry.New("http://invalid\x7f:8080", registry.WithAuth("u", "p"))
|
||||
|
||||
_, err := c.Push("assistants", "@t", "x", "1.0.0", []byte("zip"))
|
||||
if err == nil {
|
||||
t.Error("expected error from Push with invalid URL")
|
||||
}
|
||||
|
||||
_, err = c.SetTag("assistants", "@t", "x", "beta", "1.0.0")
|
||||
if err == nil {
|
||||
t.Error("expected error from SetTag with invalid URL")
|
||||
}
|
||||
|
||||
_, err = c.DeleteTag("assistants", "@t", "x", "beta")
|
||||
if err == nil {
|
||||
t.Error("expected error from DeleteTag with invalid URL")
|
||||
}
|
||||
|
||||
_, err = c.DeleteVersion("assistants", "@t", "x", "1.0.0")
|
||||
if err == nil {
|
||||
t.Error("expected error from DeleteVersion with invalid URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullNonOKStatus(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte(`{"error":"forbidden"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := registry.New(srv.URL)
|
||||
_, _, err := c.Pull("assistants", "@t", "x", "1.0.0")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for forbidden pull")
|
||||
}
|
||||
apiErr, ok := err.(*registry.APIError)
|
||||
if !ok {
|
||||
t.Fatalf("expected APIError, got %T", err)
|
||||
}
|
||||
if apiErr.StatusCode != 403 {
|
||||
t.Errorf("expected 403, got %d", apiErr.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetTagDeleteTagError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(`{"error":"bad request"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := registry.New(srv.URL, registry.WithAuth("u", "p"))
|
||||
|
||||
_, err := c.SetTag("assistants", "@t", "x", "beta", "1.0.0")
|
||||
if err == nil {
|
||||
t.Error("expected error from SetTag")
|
||||
}
|
||||
|
||||
_, err = c.DeleteTag("assistants", "@t", "x", "beta")
|
||||
if err == nil {
|
||||
t.Error("expected error from DeleteTag")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteVersionError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
w.Write([]byte(`{"error":"conflict"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := registry.New(srv.URL, registry.WithAuth("u", "p"))
|
||||
_, err := c.DeleteVersion("assistants", "@t", "x", "1.0.0")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMalformedResponseBody(t *testing.T) {
|
||||
callCount := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
callCount++
|
||||
switch {
|
||||
case r.Method == http.MethodPut && r.URL.Path != "/tags/" && callCount <= 2:
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write([]byte("not-json"))
|
||||
case r.Method == http.MethodPut:
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("not-json"))
|
||||
case r.Method == http.MethodDelete:
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("not-json"))
|
||||
default:
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("not-json"))
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := registry.New(srv.URL, registry.WithAuth("u", "p"))
|
||||
|
||||
_, err := c.Push("assistants", "@t", "x", "1.0.0", []byte("zip"))
|
||||
if err == nil {
|
||||
t.Error("expected decode error from Push")
|
||||
}
|
||||
|
||||
_, err = c.SetTag("assistants", "@t", "x", "beta", "1.0.0")
|
||||
if err == nil {
|
||||
t.Error("expected decode error from SetTag")
|
||||
}
|
||||
|
||||
_, err = c.DeleteTag("assistants", "@t", "x", "beta")
|
||||
if err == nil {
|
||||
t.Error("expected decode error from DeleteTag")
|
||||
}
|
||||
|
||||
_, err = c.DeleteVersion("assistants", "@t", "x", "1.0.0")
|
||||
if err == nil {
|
||||
t.Error("expected decode error from DeleteVersion")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullReadBodyError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Length", "99999")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("short"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := registry.New(srv.URL)
|
||||
data, _, err := c.Pull("assistants", "@t", "x", "1.0.0")
|
||||
if err != nil {
|
||||
t.Logf("got expected error: %v", err)
|
||||
return
|
||||
}
|
||||
if len(data) == 99999 {
|
||||
t.Error("expected incomplete read")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoTransportError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
closedURL := srv.URL
|
||||
srv.Close()
|
||||
|
||||
c := registry.New(closedURL, registry.WithAuth("u", "p"))
|
||||
|
||||
_, err := c.Push("assistants", "@t", "x", "1.0.0", []byte("zip"))
|
||||
if err == nil {
|
||||
t.Error("expected transport error from Push")
|
||||
}
|
||||
|
||||
_, _, err = c.Pull("assistants", "@t", "x", "1.0.0")
|
||||
if err == nil {
|
||||
t.Error("expected transport error from Pull")
|
||||
}
|
||||
|
||||
_, err = c.SetTag("assistants", "@t", "x", "beta", "1.0.0")
|
||||
if err == nil {
|
||||
t.Error("expected transport error from SetTag")
|
||||
}
|
||||
|
||||
_, err = c.DeleteTag("assistants", "@t", "x", "beta")
|
||||
if err == nil {
|
||||
t.Error("expected transport error from DeleteTag")
|
||||
}
|
||||
|
||||
_, err = c.DeleteVersion("assistants", "@t", "x", "1.0.0")
|
||||
if err == nil {
|
||||
t.Error("expected transport error from DeleteVersion")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue