feat: implement API key management with reconciliation and add update key endpoint

This commit is contained in:
CJACK.
2026-04-22 15:51:43 +00:00
parent 8ff923cd77
commit 8f09e3b381
19 changed files with 629 additions and 107 deletions

View File

@@ -234,6 +234,43 @@ func TestUpdateSettingsHotReloadTokenRefreshInterval(t *testing.T) {
}
}
func TestUpdateConfigPreservesStructuredAPIKeysWhenBothFieldsPresent(t *testing.T) {
h := newAdminTestHandler(t, `{
"keys":["legacy"],
"api_keys":[{"key":"legacy","name":"primary","remark":"prod"}],
"accounts":[]
}`)
payload := map[string]any{
"keys": []any{"legacy", "new-key"},
"api_keys": []any{
map[string]any{"key": "legacy", "name": "primary-updated", "remark": "prod-updated"},
map[string]any{"key": "new-key", "name": "secondary", "remark": "staging"},
},
}
b, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/admin/config", bytes.NewReader(b))
rec := httptest.NewRecorder()
h.updateConfig(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
snap := h.Store.Snapshot()
if len(snap.Keys) != 2 || snap.Keys[0] != "legacy" || snap.Keys[1] != "new-key" {
t.Fatalf("unexpected keys after config update: %#v", snap.Keys)
}
if len(snap.APIKeys) != 2 {
t.Fatalf("unexpected api keys after config update: %#v", snap.APIKeys)
}
if snap.APIKeys[0].Name != "primary-updated" || snap.APIKeys[0].Remark != "prod-updated" {
t.Fatalf("structured metadata for existing key was not preserved: %#v", snap.APIKeys[0])
}
if snap.APIKeys[1].Name != "secondary" || snap.APIKeys[1].Remark != "staging" {
t.Fatalf("structured metadata for new key was not preserved: %#v", snap.APIKeys[1])
}
}
func TestUpdateSettingsPasswordInvalidatesOldJWT(t *testing.T) {
hash := authn.HashAdminPassword("old-password")
h := newAdminTestHandler(t, `{"admin":{"password_hash":"`+hash+`"}}`)
@@ -315,6 +352,40 @@ func TestConfigImportMergeAndReplace(t *testing.T) {
}
}
func TestConfigImportMergePreservesStructuredAPIKeys(t *testing.T) {
h := newAdminTestHandler(t, `{
"api_keys":[{"key":"k1","name":"primary","remark":"prod"}]
}`)
merge := map[string]any{
"mode": "merge",
"config": map[string]any{
"api_keys": []any{
map[string]any{"key": "k1", "name": "should-not-overwrite", "remark": "ignored"},
map[string]any{"key": "k2", "name": "secondary", "remark": "staging"},
},
},
}
mergeBytes, _ := json.Marshal(merge)
mergeReq := httptest.NewRequest(http.MethodPost, "/admin/config/import?mode=merge", bytes.NewReader(mergeBytes))
mergeRec := httptest.NewRecorder()
h.configImport(mergeRec, mergeReq)
if mergeRec.Code != http.StatusOK {
t.Fatalf("merge status=%d body=%s", mergeRec.Code, mergeRec.Body.String())
}
snap := h.Store.Snapshot()
if len(snap.APIKeys) != 2 {
t.Fatalf("unexpected api keys after structured merge: %#v", snap.APIKeys)
}
if snap.APIKeys[0].Name != "primary" || snap.APIKeys[0].Remark != "prod" {
t.Fatalf("existing structured metadata was overwritten: %#v", snap.APIKeys[0])
}
if snap.APIKeys[1].Name != "secondary" || snap.APIKeys[1].Remark != "staging" {
t.Fatalf("new structured metadata was lost: %#v", snap.APIKeys[1])
}
}
func TestConfigImportAppliesTokenRefreshInterval(t *testing.T) {
h := newAdminTestHandler(t, `{"keys":["k1"]}`)