mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 08:57:57 +00:00
fix(webui): send DELETE when removing keys and adapters
Deleting a gateway key from the admin UI did nothing and reported "use GET /api/keys": delKey() called api() with an empty options object, so fetch defaulted to GET and the request landed in the GET branch of handleKeysAPI. delAdapter() had the identical bug and reported "adapter code not exposed; edit in UI". This is the third instance of the same mistake —ab20f1bfixed delSource and delTemplate, missing these two — so it is now pinned by tests instead of by review: * TestUIAPICallsDeclareMethod walks every api() call in the embedded index.html and fails if one passes an options object without a method (an AbortSignal-only read is allowed, being a deliberate GET). * TestUIDeleteHelpersUseDelete / TestUIMutatingHelpersUseWriteMethods pin the verb of each removal and write helper by name. * TestKeyDeleteRoundTrip covers create -> DELETE -> gone -> second DELETE is a clean 404, and TestCannotDeleteOwnKey keeps the lockout guard. The 404 bodies for GET /api/keys/{key} and GET /api/adapters/{name} now name the verb to use ("DELETE /api/keys/{key} to remove"), because that message is what a mis-methoded client actually shows its user; "use GET /api/keys" read as though the caller had done nothing wrong. delSource's indentation, broken byab20f1b, is also straightened out.
This commit is contained in:
@ -914,3 +914,88 @@ func TestStatsRecordsAPIScopedToOwnKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeyDeleteRoundTrip is the end-to-end version of the bug users hit:
|
||||
// deleting a gateway key from the admin UI. It also asserts the GET-on-a-key
|
||||
// error names the right verb, since that message is what a mis-methoded client
|
||||
// actually sees.
|
||||
func TestKeyDeleteRoundTrip(t *testing.T) {
|
||||
g := newTestGateway(t)
|
||||
|
||||
rr := doReq(t, g, http.MethodPost, "/api/keys", `{"name":"doomed","role":"user"}`)
|
||||
if rr.Code != 200 {
|
||||
t.Fatalf("create: %d %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
var created struct {
|
||||
Key struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
} `json:"key"`
|
||||
}
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &created); err != nil {
|
||||
t.Fatalf("decode create: %v", err)
|
||||
}
|
||||
if created.Key.Key == "" {
|
||||
t.Fatalf("no key returned: %s", rr.Body.String())
|
||||
}
|
||||
|
||||
// DELETE removes it
|
||||
rr = doReq(t, g, http.MethodDelete, "/api/keys/"+url.PathEscape(created.Key.Key), "")
|
||||
if rr.Code != 200 {
|
||||
t.Fatalf("delete: %d %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
for _, k := range g.core.ListKeys() {
|
||||
if k.Key == created.Key.Key {
|
||||
t.Fatal("key still present after DELETE")
|
||||
}
|
||||
}
|
||||
|
||||
// deleting again is a clean 404, not a 500
|
||||
rr = doReq(t, g, http.MethodDelete, "/api/keys/"+url.PathEscape(created.Key.Key), "")
|
||||
if rr.Code != http.StatusNotFound {
|
||||
t.Fatalf("second delete: %d, want 404", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeyGetOnSpecificKeyNamesTheVerb: a GET on /api/keys/{key} is what a
|
||||
// client that forgot to set method:"DELETE" ends up sending. The error must say
|
||||
// which verb to use instead of just "use GET /api/keys", which reads as if the
|
||||
// caller did nothing wrong.
|
||||
func TestKeyGetOnSpecificKeyNamesTheVerb(t *testing.T) {
|
||||
g := newTestGateway(t)
|
||||
rr := doReq(t, g, http.MethodGet, "/api/keys/sk-whatever", "")
|
||||
if rr.Code != http.StatusNotFound {
|
||||
t.Fatalf("status = %d, want 404", rr.Code)
|
||||
}
|
||||
body := rr.Body.String()
|
||||
for _, want := range []string{"DELETE /api/keys/", "PUT /api/keys/"} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("error message should mention %q, got: %s", want, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdapterGetOnSpecificAdapterNamesDelete is the same guard for the adapters
|
||||
// endpoint, whose delAdapter helper had the identical missing-method bug.
|
||||
func TestAdapterGetOnSpecificAdapterNamesDelete(t *testing.T) {
|
||||
g := newTestGateway(t)
|
||||
rr := doReq(t, g, http.MethodGet, "/api/adapters/openai", "")
|
||||
if rr.Code != http.StatusNotFound {
|
||||
t.Fatalf("status = %d, want 404", rr.Code)
|
||||
}
|
||||
if !strings.Contains(rr.Body.String(), "DELETE /api/adapters/openai") {
|
||||
t.Errorf("error should name the DELETE verb, got: %s", rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestCannotDeleteOwnKey keeps the admin from locking themselves out.
|
||||
func TestCannotDeleteOwnKey(t *testing.T) {
|
||||
g := newTestGateway(t)
|
||||
rr := doReq(t, g, http.MethodDelete, "/api/keys/sk-test", "")
|
||||
if rr.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want 400", rr.Code)
|
||||
}
|
||||
if !strings.Contains(rr.Body.String(), "logged in with") {
|
||||
t.Errorf("unexpected message: %s", rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user