mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
Add `POST /admin/masters/{id}/keys` allowing admins to issue child keys
on behalf of a master. Introduce an `issued_by` field in the Key model
to audit whether a key was issued by the master or an admin.
Refactor master service to use typed errors for consistent HTTP status
mapping and ensure validation logic (active status, group check) is
shared.
86 lines
2.3 KiB
Go
86 lines
2.3 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/alicebob/miniredis/v2"
|
|
"github.com/ez-api/ez-api/internal/model"
|
|
"github.com/ez-api/ez-api/internal/service"
|
|
"github.com/ez-api/foundation/tokenhash"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/redis/go-redis/v9"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func TestAdmin_IssueChildKeyForMaster_IssuedByAdminAndSynced(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open sqlite: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(&model.Master{}, &model.Key{}); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
|
|
mr := miniredis.RunT(t)
|
|
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
|
|
|
syncService := service.NewSyncService(rdb)
|
|
masterService := service.NewMasterService(db)
|
|
adminHandler := NewAdminHandler(masterService, syncService)
|
|
|
|
m, _, err := masterService.CreateMaster("m1", "default", 5, 10)
|
|
if err != nil {
|
|
t.Fatalf("CreateMaster: %v", err)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.POST("/admin/masters/:id/keys", adminHandler.IssueChildKeyForMaster)
|
|
|
|
body := []byte(`{"scopes":"chat:write"}`)
|
|
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/admin/masters/%d/keys", m.ID), bytes.NewReader(body))
|
|
rr := httptest.NewRecorder()
|
|
r.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusCreated {
|
|
t.Fatalf("unexpected status: got=%d body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if resp["issued_by"] != "admin" {
|
|
t.Fatalf("expected issued_by=admin, got=%v", resp["issued_by"])
|
|
}
|
|
rawKey, _ := resp["key_secret"].(string)
|
|
if rawKey == "" {
|
|
t.Fatalf("expected key_secret in response")
|
|
}
|
|
|
|
// DB audit
|
|
var keys []model.Key
|
|
if err := db.Where("master_id = ?", m.ID).Find(&keys).Error; err != nil {
|
|
t.Fatalf("load keys: %v", err)
|
|
}
|
|
if len(keys) != 1 {
|
|
t.Fatalf("expected 1 key row, got %d", len(keys))
|
|
}
|
|
if keys[0].IssuedBy != "admin" {
|
|
t.Fatalf("expected IssuedBy=admin, got %q", keys[0].IssuedBy)
|
|
}
|
|
|
|
// Redis sync: auth:token:{hash} exists
|
|
hash := tokenhash.HashToken(rawKey)
|
|
if !mr.Exists("auth:token:" + hash) {
|
|
t.Fatalf("expected auth token hash key to exist in redis")
|
|
}
|
|
}
|