486 lines
16 KiB
Go
486 lines
16 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/model"
|
|
)
|
|
|
|
// --- POST /api/v1/users ---
|
|
|
|
func TestCreateUser_Success(t *testing.T) {
|
|
kv, _, _, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
|
|
body := `{"user":"alice"}`
|
|
rr := doRequest(router, http.MethodPost, "/api/v1/users", body, "dpk_admin")
|
|
|
|
if rr.Code != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var resp createUserResponse
|
|
decodeJSON(t, rr, &resp)
|
|
if resp.User.ID != "alice" {
|
|
t.Fatalf("expected user id 'alice', got %q", resp.User.ID)
|
|
}
|
|
if resp.APIKey == "" {
|
|
t.Fatal("expected api_key to be set")
|
|
}
|
|
if resp.APIKey != "dpk_testgeneratedkey123456" {
|
|
t.Fatalf("expected mock api key, got %q", resp.APIKey)
|
|
}
|
|
// Default quotas should be applied
|
|
if resp.User.Quota.MaxConcurrentPods != 3 {
|
|
t.Fatalf("expected default max_concurrent_pods 3, got %d", resp.User.Quota.MaxConcurrentPods)
|
|
}
|
|
}
|
|
|
|
func TestCreateUser_WithCustomQuotas(t *testing.T) {
|
|
kv, _, _, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
|
|
body := `{"user":"bob","quotas":{"max_concurrent_pods":5,"max_cpu_per_pod":16,"max_ram_gb_per_pod":32,"monthly_pod_hours":1000,"monthly_ai_requests":50000}}`
|
|
rr := doRequest(router, http.MethodPost, "/api/v1/users", body, "dpk_admin")
|
|
|
|
if rr.Code != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var resp createUserResponse
|
|
decodeJSON(t, rr, &resp)
|
|
if resp.User.Quota.MaxConcurrentPods != 5 {
|
|
t.Fatalf("expected max_concurrent_pods 5, got %d", resp.User.Quota.MaxConcurrentPods)
|
|
}
|
|
if resp.User.Quota.MaxCPUPerPod != 16 {
|
|
t.Fatalf("expected max_cpu_per_pod 16, got %d", resp.User.Quota.MaxCPUPerPod)
|
|
}
|
|
}
|
|
|
|
func TestCreateUser_ForbiddenNonAdmin(t *testing.T) {
|
|
kv, _, _, router := newPodTestRouter()
|
|
user := &model.User{ID: "alice", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_test", user, model.RoleUser)
|
|
|
|
body := `{"user":"newuser"}`
|
|
rr := doRequest(router, http.MethodPost, "/api/v1/users", body, "dpk_test")
|
|
|
|
if rr.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestCreateUser_InvalidBody(t *testing.T) {
|
|
kv, _, _, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
|
|
rr := doRequest(router, http.MethodPost, "/api/v1/users", "not json", "dpk_admin")
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestCreateUser_ValidationError(t *testing.T) {
|
|
kv, _, _, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
|
|
// Missing required field
|
|
rr := doRequest(router, http.MethodPost, "/api/v1/users", `{}`, "dpk_admin")
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestCreateUser_InvalidName(t *testing.T) {
|
|
kv, _, _, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
|
|
rr := doRequest(router, http.MethodPost, "/api/v1/users", `{"user":"INVALID_NAME"}`, "dpk_admin")
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestCreateUser_Duplicate(t *testing.T) {
|
|
kv, _, us, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
us.addUser(&model.User{ID: "alice", Quota: model.DefaultQuota()})
|
|
|
|
body := `{"user":"alice"}`
|
|
rr := doRequest(router, http.MethodPost, "/api/v1/users", body, "dpk_admin")
|
|
if rr.Code != http.StatusConflict {
|
|
t.Fatalf("expected 409, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestCreateUser_PartialQuotasMergedWithDefaults(t *testing.T) {
|
|
kv, _, _, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
|
|
// Only specify max_cpu_per_pod; other fields should get defaults
|
|
body := `{"user":"dave","quotas":{"max_cpu_per_pod":16}}`
|
|
rr := doRequest(router, http.MethodPost, "/api/v1/users", body, "dpk_admin")
|
|
|
|
if rr.Code != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var resp createUserResponse
|
|
decodeJSON(t, rr, &resp)
|
|
if resp.User.Quota.MaxCPUPerPod != 16 {
|
|
t.Fatalf("expected max_cpu_per_pod 16, got %d", resp.User.Quota.MaxCPUPerPod)
|
|
}
|
|
// Unset fields should be defaults, not zero
|
|
if resp.User.Quota.MaxConcurrentPods != 3 {
|
|
t.Fatalf("expected default max_concurrent_pods 3, got %d", resp.User.Quota.MaxConcurrentPods)
|
|
}
|
|
if resp.User.Quota.MaxRAMGBPerPod != 16 {
|
|
t.Fatalf("expected default max_ram_gb_per_pod 16, got %d", resp.User.Quota.MaxRAMGBPerPod)
|
|
}
|
|
if resp.User.Quota.MonthlyPodHours != 500 {
|
|
t.Fatalf("expected default monthly_pod_hours 500, got %d", resp.User.Quota.MonthlyPodHours)
|
|
}
|
|
if resp.User.Quota.MonthlyAIRequests != 10000 {
|
|
t.Fatalf("expected default monthly_ai_requests 10000, got %d", resp.User.Quota.MonthlyAIRequests)
|
|
}
|
|
}
|
|
|
|
func TestCreateUser_NegativeQuotaRejected(t *testing.T) {
|
|
kv, _, _, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
|
|
body := `{"user":"eve","quotas":{"max_cpu_per_pod":-1}}`
|
|
rr := doRequest(router, http.MethodPost, "/api/v1/users", body, "dpk_admin")
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestCreateUser_APIKeyFailureRollsBackUser(t *testing.T) {
|
|
kv, _, us, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
|
|
// Make CreateAPIKey fail
|
|
us.createAPIKeyErr = fmt.Errorf("db connection lost")
|
|
|
|
body := `{"user":"frank"}`
|
|
rr := doRequest(router, http.MethodPost, "/api/v1/users", body, "dpk_admin")
|
|
if rr.Code != http.StatusInternalServerError {
|
|
t.Fatalf("expected 500, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
// User should have been cleaned up — not left orphaned
|
|
if _, exists := us.users["frank"]; exists {
|
|
t.Fatal("expected user 'frank' to be cleaned up after API key failure")
|
|
}
|
|
}
|
|
|
|
func TestDeleteUser_FailsWhenPodDeletionFails(t *testing.T) {
|
|
kv, pm, us, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
us.addUser(&model.User{ID: "alice", Quota: model.DefaultQuota()})
|
|
pm.addPod(&model.Pod{User: "alice", Name: "main"})
|
|
|
|
// Make pod deletion fail
|
|
pm.deleteAllPodsErr = fmt.Errorf("k8s API unreachable")
|
|
|
|
rr := doRequest(router, http.MethodDelete, "/api/v1/users/alice", "", "dpk_admin")
|
|
if rr.Code != http.StatusInternalServerError {
|
|
t.Fatalf("expected 500 when pod deletion fails, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
// User record should still exist (not deleted)
|
|
if _, exists := us.users["alice"]; !exists {
|
|
t.Fatal("expected user 'alice' to still exist after pod deletion failure")
|
|
}
|
|
}
|
|
|
|
// --- GET /api/v1/users ---
|
|
|
|
func TestListUsers_AdminSeesAll(t *testing.T) {
|
|
kv, _, us, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
us.addUser(&model.User{ID: "alice", Quota: model.DefaultQuota()})
|
|
us.addUser(&model.User{ID: "bob", Quota: model.DefaultQuota()})
|
|
|
|
rr := doRequest(router, http.MethodGet, "/api/v1/users", "", "dpk_admin")
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var users []model.User
|
|
decodeJSON(t, rr, &users)
|
|
if len(users) != 2 {
|
|
t.Fatalf("expected 2 users, got %d", len(users))
|
|
}
|
|
}
|
|
|
|
func TestListUsers_UserSeesSelf(t *testing.T) {
|
|
kv, _, us, router := newPodTestRouter()
|
|
alice := &model.User{ID: "alice", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_alice", alice, model.RoleUser)
|
|
us.addUser(alice)
|
|
us.addUser(&model.User{ID: "bob", Quota: model.DefaultQuota()})
|
|
|
|
rr := doRequest(router, http.MethodGet, "/api/v1/users", "", "dpk_alice")
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var users []model.User
|
|
decodeJSON(t, rr, &users)
|
|
if len(users) != 1 {
|
|
t.Fatalf("expected 1 user (self), got %d", len(users))
|
|
}
|
|
if users[0].ID != "alice" {
|
|
t.Fatalf("expected user 'alice', got %q", users[0].ID)
|
|
}
|
|
}
|
|
|
|
// --- GET /api/v1/users/{user} ---
|
|
|
|
func TestGetUser_Self(t *testing.T) {
|
|
kv, _, us, router := newPodTestRouter()
|
|
alice := &model.User{ID: "alice", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_alice", alice, model.RoleUser)
|
|
us.addUser(alice)
|
|
|
|
rr := doRequest(router, http.MethodGet, "/api/v1/users/alice", "", "dpk_alice")
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var user model.User
|
|
decodeJSON(t, rr, &user)
|
|
if user.ID != "alice" {
|
|
t.Fatalf("expected user 'alice', got %q", user.ID)
|
|
}
|
|
}
|
|
|
|
func TestGetUser_AdminCanViewOthers(t *testing.T) {
|
|
kv, _, us, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
us.addUser(&model.User{ID: "alice", Quota: model.DefaultQuota()})
|
|
|
|
rr := doRequest(router, http.MethodGet, "/api/v1/users/alice", "", "dpk_admin")
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestGetUser_ForbiddenOtherUser(t *testing.T) {
|
|
kv, _, us, router := newPodTestRouter()
|
|
alice := &model.User{ID: "alice", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_alice", alice, model.RoleUser)
|
|
us.addUser(&model.User{ID: "bob", Quota: model.DefaultQuota()})
|
|
|
|
rr := doRequest(router, http.MethodGet, "/api/v1/users/bob", "", "dpk_alice")
|
|
if rr.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestGetUser_NotFound(t *testing.T) {
|
|
kv, _, _, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
|
|
rr := doRequest(router, http.MethodGet, "/api/v1/users/nonexistent", "", "dpk_admin")
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// --- DELETE /api/v1/users/{user} ---
|
|
|
|
func TestDeleteUser_Success(t *testing.T) {
|
|
kv, pm, us, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
us.addUser(&model.User{ID: "alice", Quota: model.DefaultQuota()})
|
|
pm.addPod(&model.Pod{User: "alice", Name: "main"})
|
|
|
|
rr := doRequest(router, http.MethodDelete, "/api/v1/users/alice", "", "dpk_admin")
|
|
if rr.Code != http.StatusNoContent {
|
|
t.Fatalf("expected 204, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
// Verify user is gone
|
|
getRR := doRequest(router, http.MethodGet, "/api/v1/users/alice", "", "dpk_admin")
|
|
if getRR.Code != http.StatusNotFound {
|
|
t.Fatalf("expected user to be deleted, got status %d", getRR.Code)
|
|
}
|
|
|
|
// Verify pods are gone
|
|
pods, _ := pm.ListPods(nil, "alice")
|
|
if len(pods) != 0 {
|
|
t.Fatalf("expected pods to be deleted, got %d", len(pods))
|
|
}
|
|
}
|
|
|
|
func TestDeleteUser_ForbiddenNonAdmin(t *testing.T) {
|
|
kv, _, us, router := newPodTestRouter()
|
|
alice := &model.User{ID: "alice", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_alice", alice, model.RoleUser)
|
|
us.addUser(alice)
|
|
|
|
rr := doRequest(router, http.MethodDelete, "/api/v1/users/alice", "", "dpk_alice")
|
|
if rr.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestDeleteUser_NotFound(t *testing.T) {
|
|
kv, _, _, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
|
|
rr := doRequest(router, http.MethodDelete, "/api/v1/users/nonexistent", "", "dpk_admin")
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// --- PATCH /api/v1/users/{user}/quotas ---
|
|
|
|
func TestUpdateQuotas_Success(t *testing.T) {
|
|
kv, _, us, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
us.addUser(&model.User{ID: "alice", Quota: model.DefaultQuota()})
|
|
|
|
body := `{"max_concurrent_pods":10,"max_cpu_per_pod":16}`
|
|
rr := doRequest(router, http.MethodPatch, "/api/v1/users/alice/quotas", body, "dpk_admin")
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var user model.User
|
|
decodeJSON(t, rr, &user)
|
|
if user.Quota.MaxConcurrentPods != 10 {
|
|
t.Fatalf("expected max_concurrent_pods 10, got %d", user.Quota.MaxConcurrentPods)
|
|
}
|
|
if user.Quota.MaxCPUPerPod != 16 {
|
|
t.Fatalf("expected max_cpu_per_pod 16, got %d", user.Quota.MaxCPUPerPod)
|
|
}
|
|
// Unchanged fields should be preserved
|
|
if user.Quota.MonthlyPodHours != 500 {
|
|
t.Fatalf("expected monthly_pod_hours 500, got %d", user.Quota.MonthlyPodHours)
|
|
}
|
|
}
|
|
|
|
func TestUpdateQuotas_ForbiddenNonAdmin(t *testing.T) {
|
|
kv, _, us, router := newPodTestRouter()
|
|
alice := &model.User{ID: "alice", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_alice", alice, model.RoleUser)
|
|
us.addUser(alice)
|
|
|
|
body := `{"max_concurrent_pods":10}`
|
|
rr := doRequest(router, http.MethodPatch, "/api/v1/users/alice/quotas", body, "dpk_alice")
|
|
if rr.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestUpdateQuotas_InvalidBody(t *testing.T) {
|
|
kv, _, _, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
|
|
rr := doRequest(router, http.MethodPatch, "/api/v1/users/alice/quotas", "not json", "dpk_admin")
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestUpdateQuotas_ValidationError(t *testing.T) {
|
|
kv, _, _, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
|
|
// max_concurrent_pods must be at least 1
|
|
body := `{"max_concurrent_pods":0}`
|
|
rr := doRequest(router, http.MethodPatch, "/api/v1/users/alice/quotas", body, "dpk_admin")
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestUpdateQuotas_UserNotFound(t *testing.T) {
|
|
kv, _, _, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
|
|
body := `{"max_concurrent_pods":10}`
|
|
rr := doRequest(router, http.MethodPatch, "/api/v1/users/nonexistent/quotas", body, "dpk_admin")
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// --- Auth integration for user routes ---
|
|
|
|
func TestUserRoutes_NoAuth(t *testing.T) {
|
|
_, _, _, router := newPodTestRouter()
|
|
|
|
routes := []struct {
|
|
method string
|
|
path string
|
|
}{
|
|
{http.MethodPost, "/api/v1/users"},
|
|
{http.MethodGet, "/api/v1/users"},
|
|
{http.MethodGet, "/api/v1/users/alice"},
|
|
{http.MethodDelete, "/api/v1/users/alice"},
|
|
{http.MethodPatch, "/api/v1/users/alice/quotas"},
|
|
}
|
|
|
|
for _, rt := range routes {
|
|
t.Run(rt.method+" "+rt.path, func(t *testing.T) {
|
|
rr := doRequest(router, rt.method, rt.path, "", "")
|
|
if rr.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", rr.Code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- verify response structure ---
|
|
|
|
func TestCreateUser_ResponseStructure(t *testing.T) {
|
|
kv, _, _, router := newPodTestRouter()
|
|
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
|
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
|
|
|
body := `{"user":"charlie"}`
|
|
rr := doRequest(router, http.MethodPost, "/api/v1/users", body, "dpk_admin")
|
|
if rr.Code != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
// Verify response has both user and api_key fields
|
|
var raw map[string]json.RawMessage
|
|
if err := json.NewDecoder(rr.Body).Decode(&raw); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if _, ok := raw["user"]; !ok {
|
|
t.Fatal("response missing 'user' field")
|
|
}
|
|
if _, ok := raw["api_key"]; !ok {
|
|
t.Fatal("response missing 'api_key' field")
|
|
}
|
|
}
|