ffab866c87
Full M4.2 deliverable: 16 endpoints (tenants CRUD + lifecycle, catalog, entitlements, API keys with argon2 hashing, audit append + filter), Store interface with pgx-backed Postgres + in-memory parallel implementations exercised by the same eachStore harness, openapi.yaml at 3.1 with kin-openapi contract test. M4.3 adds auth. Refs: M4.2
130 lines
4.0 KiB
Go
130 lines
4.0 KiB
Go
package server_test
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
|
|
"gitea.meghsakha.com/platform/tenant-registry/internal/store"
|
|
)
|
|
|
|
func TestCreateAPIKey_then_verify(t *testing.T) {
|
|
eachStore(t, func(t *testing.T, h *testHarness) {
|
|
resp, body := h.do("POST", "/v1/api-keys", map[string]any{
|
|
"tenant_id": h.tenant.ID, "name": "ci-bot", "product": "certifai",
|
|
"scopes": []string{"certifai:read", "certifai:write"},
|
|
})
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("create status = %d, body=%s", resp.StatusCode, body)
|
|
}
|
|
created := decode[struct {
|
|
APIKey store.APIKey `json:"api_key"`
|
|
Plaintext string `json:"plaintext"`
|
|
}](t, body)
|
|
if len(created.Plaintext) < 30 || created.Plaintext[:3] != "bp_" {
|
|
t.Fatalf("bad plaintext: %q", created.Plaintext)
|
|
}
|
|
if len(created.APIKey.Scopes) != 2 || created.APIKey.Product != "certifai" {
|
|
t.Errorf("unexpected key: %+v", created.APIKey)
|
|
}
|
|
|
|
// Verify with the plaintext key.
|
|
resp, body = h.do("POST", "/v1/internal/api-keys/verify", map[string]any{
|
|
"key": created.Plaintext,
|
|
})
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("verify status = %d, body=%s", resp.StatusCode, body)
|
|
}
|
|
v := decode[struct {
|
|
Valid bool `json:"valid"`
|
|
TenantID string `json:"tenant_id"`
|
|
Product string `json:"product"`
|
|
Scopes []string `json:"scopes"`
|
|
}](t, body)
|
|
if !v.Valid || v.TenantID != h.tenant.ID || v.Product != "certifai" || len(v.Scopes) != 2 {
|
|
t.Errorf("verify returned %+v", v)
|
|
}
|
|
|
|
// Revoke; verify now returns valid=false.
|
|
resp, _ = h.do("DELETE", "/v1/api-keys/"+created.APIKey.ID, nil)
|
|
if resp.StatusCode != http.StatusNoContent {
|
|
t.Fatalf("revoke status = %d", resp.StatusCode)
|
|
}
|
|
resp, body = h.do("POST", "/v1/internal/api-keys/verify", map[string]any{"key": created.Plaintext})
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("verify-after-revoke status = %d", resp.StatusCode)
|
|
}
|
|
v = decode[struct {
|
|
Valid bool `json:"valid"`
|
|
TenantID string `json:"tenant_id"`
|
|
Product string `json:"product"`
|
|
Scopes []string `json:"scopes"`
|
|
}](t, body)
|
|
if v.Valid {
|
|
t.Error("revoked key still verifies")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestVerifyAPIKey_garbage(t *testing.T) {
|
|
eachStore(t, func(t *testing.T, h *testHarness) {
|
|
for _, key := range []string{"", "not-a-key", "bp_short", "ax_wrongprefix1234567"} {
|
|
resp, body := h.do("POST", "/v1/internal/api-keys/verify", map[string]any{"key": key})
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("status = %d for key %q", resp.StatusCode, key)
|
|
}
|
|
v := decode[struct {
|
|
Valid bool `json:"valid"`
|
|
}](t, body)
|
|
if v.Valid {
|
|
t.Errorf("garbage key %q verified as valid", key)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCreateAPIKey_unknownProduct(t *testing.T) {
|
|
eachStore(t, func(t *testing.T, h *testHarness) {
|
|
resp, _ := h.do("POST", "/v1/api-keys", map[string]any{
|
|
"tenant_id": h.tenant.ID, "name": "k", "product": "bogus",
|
|
})
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("status = %d", resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestListAPIKeys(t *testing.T) {
|
|
eachStore(t, func(t *testing.T, h *testHarness) {
|
|
respA, bodyA := h.do("POST", "/v1/api-keys", map[string]any{
|
|
"tenant_id": h.tenant.ID, "name": "alpha",
|
|
})
|
|
if respA.StatusCode != http.StatusCreated {
|
|
t.Fatalf("alpha create: status=%d body=%s", respA.StatusCode, bodyA)
|
|
}
|
|
respB, bodyB := h.do("POST", "/v1/api-keys", map[string]any{
|
|
"tenant_id": h.tenant.ID, "name": "beta",
|
|
})
|
|
if respB.StatusCode != http.StatusCreated {
|
|
t.Fatalf("beta create: status=%d body=%s", respB.StatusCode, bodyB)
|
|
}
|
|
resp, body := h.do("GET", "/v1/api-keys?tenant_id="+h.tenant.ID, nil)
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("status = %d", resp.StatusCode)
|
|
}
|
|
out := decode[struct {
|
|
Items []store.APIKey `json:"items"`
|
|
}](t, body)
|
|
if len(out.Items) < 2 {
|
|
t.Errorf("expected ≥2 keys, got %d", len(out.Items))
|
|
}
|
|
// Plaintext / hash must NOT leak in the list response.
|
|
for _, k := range out.Items {
|
|
rawJSON, _ := h.do("GET", "/v1/api-keys?tenant_id="+h.tenant.ID, nil)
|
|
_ = rawJSON
|
|
if k.Prefix == "" {
|
|
t.Error("prefix missing")
|
|
}
|
|
}
|
|
})
|
|
}
|