mirror of
https://github.com/hanzoai/compute.git
synced 2026-10-30 14:50:41 +00:00
GET /v1/get-assets GET /v1/assets
POST /v1/add-asset -> POST /v1/assets
GET /v1/get-asset GET /v1/assets/:owner/:name
POST /v1/update-asset PUT /v1/assets/:owner/:name
POST /v1/delete-asset DELETE /v1/assets/:owner/:name
and the same five for providers. The verb is the METHOD now: replacing an item
is PUT and removing one is DELETE, where the old surface said POST to both.
The ten old addresses answer 410 and name their successor. They are rows in the
one retirement table; the mechanism is already there.
WHY THE SUCCESSOR IS A COLLECTION. RFC 8288 wants a URI reference in a Link, and
"/v1/assets/:owner/:name" is a router pattern — neither a URL nor an RFC 6570
template. Where the old address named one thing, the successor is the collection
it belongs to: the caller holds its own key and addresses the item from there.
TESTS. TestRetiredAddressesAnswerGone drives every retired address on every
method, unauthenticated, and asserts 410 plus the three headers. Its mutation is
the real bug: registering the retirement inside registerAPI instead of ahead of
the filters turns all of them into 403, which is what the caller would meet.
TestRetiredAddressesAreUndeclared asserts absence from the declaration with the
SUCCESSOR'S PRESENCE as the control — without that it passes over an empty
declaration, which is what a broken Route() produces.
TestAPIContractPreserved learned that retirement is not a drop: an address may
leave the table only by being retired, and one that leaves without a successor
is the silent break the test exists for. TestAPIContractVerbMix records the
shape change with its reason — four POSTs became two PUTs and two DELETEs.
CALLERS MOVED WITH THE ADDRESSES. web/src/backend/{Asset,Provider}Backend.js now
call the resource addresses, and delete no longer sends the whole object as a
body to say which one. routers/record.go's exclusion list follows.
11 packages green, vet and gofmt clean, no old spelling left outside the
retirement table.
88 lines
3.0 KiB
Go
88 lines
3.0 KiB
Go
// Copyright 2023 Hanzo Industries Inc. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package routers
|
|
|
|
import (
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/zap-proto/zip"
|
|
)
|
|
|
|
// A RETIRED ADDRESS ANSWERS 410 AND NAMES ITS SUCCESSOR — unauthenticated, on
|
|
// every method.
|
|
//
|
|
// Unauthenticated is the point. These addresses are registered ahead of
|
|
// ApiFilter, because behind it a caller learns about a retired address from the
|
|
// POLICY rather than from the resource: measured on an earlier attempt, one
|
|
// retired address answered 410 on GET and 403 on the other five, purely because
|
|
// a Casbin row happened to carry GET.
|
|
func TestRetiredAddressesAnswerGone(t *testing.T) {
|
|
app := zip.New(zip.Config{AppName: "gone", DisableStartupMessage: true})
|
|
Route(app)
|
|
if err := app.Build(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(successor) == 0 {
|
|
t.Fatal("no retirements registered — this test would pass over an empty table")
|
|
}
|
|
for path, to := range successor {
|
|
for _, m := range []string{"GET", "POST", "PUT", "PATCH", "DELETE"} {
|
|
res, err := app.Fiber().Test(httptest.NewRequest(m, path, nil))
|
|
if err != nil {
|
|
t.Fatalf("%s %s: %v", m, path, err)
|
|
}
|
|
_ = res.Body.Close()
|
|
if res.StatusCode != 410 {
|
|
t.Errorf("%s %s = %d, want 410 — a retired address is refused by the filter, "+
|
|
"not answered by the retirement", m, path, res.StatusCode)
|
|
}
|
|
if link := res.Header.Get("Link"); !strings.Contains(link, `rel="successor-version"`) ||
|
|
!strings.Contains(link, to) {
|
|
t.Errorf("%s %s Link = %q, want one naming %s", m, path, link, to)
|
|
}
|
|
if res.Header.Get("Deprecation") == "" || res.Header.Get("Sunset") == "" {
|
|
t.Errorf("%s %s: missing Deprecation or Sunset", m, path)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// A RETIRED ADDRESS IS IN NO PROJECTION, AND ITS SUCCESSOR IS.
|
|
//
|
|
// The successor's presence is the control. Without it this passes over a
|
|
// declaration that lists nothing, which is what a broken Route() produces.
|
|
func TestRetiredAddressesAreUndeclared(t *testing.T) {
|
|
app := zip.New(zip.Config{AppName: "gone", DisableStartupMessage: true})
|
|
Route(app)
|
|
if err := app.Build(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
declared := map[string]bool{}
|
|
for _, r := range app.Declaration().Routes {
|
|
declared[r.Pattern] = true
|
|
}
|
|
for path, to := range successor {
|
|
if declared[path] {
|
|
t.Errorf("%s is retired but declared — it would reach the document, the MCP tool "+
|
|
"list, the CLI and every SDK", path)
|
|
}
|
|
if !declared[to] {
|
|
t.Errorf("successor %s is not declared; the absence above proves nothing", to)
|
|
}
|
|
}
|
|
}
|