Files
garage-ui/backend/internal/handlers/users_test.go
T

368 lines
11 KiB
Go

package handlers
import (
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"Noooste/garage-ui/internal/models"
"Noooste/garage-ui/internal/services/mocks"
"github.com/gofiber/fiber/v3"
)
func newUsersTestApp(t *testing.T) (*fiber.App, *mocks.AdminMock) {
t.Helper()
admin := &mocks.AdminMock{}
h := NewUserHandler(admin)
app := fiber.New()
app.Get("/users", h.ListUsers)
app.Post("/users", h.CreateUser)
app.Get("/users/:access_key", h.GetUser)
app.Get("/users/:access_key/secret", h.GetUserSecretKey)
app.Delete("/users/:access_key", h.DeleteUser)
app.Patch("/users/:access_key", h.UpdateUserPermissions)
return app, admin
}
// --- ListUsers ---
func TestListUsers_MapsAndSkipsFailed(t *testing.T) {
app, admin := newUsersTestApp(t)
admin.ListKeysFn = func(_ context.Context) ([]models.ListKeysResponseItem, error) {
return []models.ListKeysResponseItem{
{ID: "AKIA-1", Name: "one"},
{ID: "AKIA-2", Name: "bad-detail"},
{ID: "AKIA-3", Name: "three"},
}, nil
}
admin.GetKeyInfoFn = func(_ context.Context, id string, showSecret bool) (*models.GarageKeyInfo, error) {
if showSecret {
t.Error("ListUsers must not request secret")
}
if id == "AKIA-2" {
return nil, errors.New("forbidden")
}
return &models.GarageKeyInfo{AccessKeyID: id, Name: id, Expired: false}, nil
}
resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/users", nil))
if err != nil {
t.Fatalf("app.Test: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d", resp.StatusCode)
}
var body struct {
Data models.UserListResponse `json:"data"`
}
decodeJSON(t, resp.Body, &body)
if body.Data.Count != 2 {
t.Errorf("count = %d, want 2 (failed detail skipped)", body.Data.Count)
}
}
func TestListUsers_ListError500(t *testing.T) {
app, admin := newUsersTestApp(t)
admin.ListKeysFn = func(_ context.Context) ([]models.ListKeysResponseItem, error) {
return nil, errors.New("boom")
}
resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/users", nil))
if err != nil {
t.Fatalf("app.Test: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusInternalServerError {
t.Fatalf("status = %d, want 500", resp.StatusCode)
}
}
// --- CreateUser ---
func TestCreateUser_Success201(t *testing.T) {
app, admin := newUsersTestApp(t)
admin.CreateKeyFn = func(_ context.Context, req models.CreateKeyRequest) (*models.GarageKeyInfo, error) {
if req.Name == nil || *req.Name != "alice" {
t.Errorf("Name = %v", req.Name)
}
sk := "secret-xyz"
return &models.GarageKeyInfo{AccessKeyID: "AKIA-1", Name: "alice", SecretAccessKey: &sk}, nil
}
body, _ := json.Marshal(models.CreateUserRequest{Name: "alice"})
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
t.Fatalf("status = %d, want 201", resp.StatusCode)
}
var decoded struct {
Data models.UserInfo `json:"data"`
}
decodeJSON(t, resp.Body, &decoded)
if decoded.Data.SecretKey == nil || *decoded.Data.SecretKey != "secret-xyz" {
t.Errorf("SecretKey = %v, want secret-xyz", decoded.Data.SecretKey)
}
}
func TestCreateUser_MalformedJSON400(t *testing.T) {
app, _ := newUsersTestApp(t)
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader("{not-json"))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("status = %d, want 400", resp.StatusCode)
}
}
func TestCreateUser_AdminError500(t *testing.T) {
app, admin := newUsersTestApp(t)
admin.CreateKeyFn = func(_ context.Context, _ models.CreateKeyRequest) (*models.GarageKeyInfo, error) {
return nil, errors.New("boom")
}
body, _ := json.Marshal(models.CreateUserRequest{})
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusInternalServerError {
t.Fatalf("status = %d, want 500", resp.StatusCode)
}
}
// --- GetUser ---
func TestGetUser_Success(t *testing.T) {
app, admin := newUsersTestApp(t)
admin.GetKeyInfoFn = func(_ context.Context, id string, showSecret bool) (*models.GarageKeyInfo, error) {
if showSecret {
t.Error("GetUser must not request secret")
}
return &models.GarageKeyInfo{AccessKeyID: id, Name: "alice"}, nil
}
resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/users/AKIA-1", nil))
if err != nil {
t.Fatalf("app.Test: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d", resp.StatusCode)
}
}
func TestGetUser_ServiceError500(t *testing.T) {
app, admin := newUsersTestApp(t)
admin.GetKeyInfoFn = func(_ context.Context, _ string, _ bool) (*models.GarageKeyInfo, error) {
return nil, errors.New("boom")
}
resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/users/AKIA-1", nil))
if err != nil {
t.Fatalf("app.Test: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusInternalServerError {
t.Fatalf("status = %d, want 500", resp.StatusCode)
}
}
// --- GetUserSecretKey ---
func TestGetUserSecretKey_Success(t *testing.T) {
app, admin := newUsersTestApp(t)
admin.GetKeyInfoFn = func(_ context.Context, id string, showSecret bool) (*models.GarageKeyInfo, error) {
if !showSecret {
t.Error("GetUserSecretKey must request secret")
}
sk := "s3cr3t"
return &models.GarageKeyInfo{AccessKeyID: id, SecretAccessKey: &sk}, nil
}
resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/users/AKIA-1/secret", nil))
if err != nil {
t.Fatalf("app.Test: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d", resp.StatusCode)
}
var body struct {
Data map[string]string `json:"data"`
}
decodeJSON(t, resp.Body, &body)
if body.Data["secretKey"] != "s3cr3t" {
t.Errorf("secretKey = %q", body.Data["secretKey"])
}
}
// --- DeleteUser ---
func TestDeleteUser_Success(t *testing.T) {
app, admin := newUsersTestApp(t)
admin.DeleteKeyFn = func(_ context.Context, id string) error {
if id != "AKIA-1" {
t.Errorf("id = %q", id)
}
return nil
}
resp, err := app.Test(httptest.NewRequest(http.MethodDelete, "/users/AKIA-1", nil))
if err != nil {
t.Fatalf("app.Test: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d", resp.StatusCode)
}
}
func TestDeleteUser_ServiceError500(t *testing.T) {
app, admin := newUsersTestApp(t)
admin.DeleteKeyFn = func(_ context.Context, _ string) error { return errors.New("boom") }
resp, err := app.Test(httptest.NewRequest(http.MethodDelete, "/users/AKIA-1", nil))
if err != nil {
t.Fatalf("app.Test: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusInternalServerError {
t.Fatalf("status = %d, want 500", resp.StatusCode)
}
}
// --- UpdateUserPermissions ---
func TestUpdateUser_StatusInactiveSetsPastExpiration(t *testing.T) {
app, admin := newUsersTestApp(t)
admin.UpdateKeyFn = func(_ context.Context, _ string, req models.UpdateKeyRequest) (*models.GarageKeyInfo, error) {
if req.NeverExpires {
t.Error("NeverExpires should be false when deactivating")
}
if req.Expiration == nil || !req.Expiration.Before(time.Now()) {
t.Errorf("Expiration = %v, want past time", req.Expiration)
}
return &models.GarageKeyInfo{}, nil
}
status := "inactive"
body, _ := json.Marshal(models.UpdateUserRequest{Status: &status})
req := httptest.NewRequest(http.MethodPatch, "/users/AKIA-1", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d", resp.StatusCode)
}
}
func TestUpdateUser_StatusActiveSetsNeverExpires(t *testing.T) {
app, admin := newUsersTestApp(t)
admin.UpdateKeyFn = func(_ context.Context, _ string, req models.UpdateKeyRequest) (*models.GarageKeyInfo, error) {
if !req.NeverExpires {
t.Error("NeverExpires should be true when activating")
}
return &models.GarageKeyInfo{}, nil
}
status := "active"
body, _ := json.Marshal(models.UpdateUserRequest{Status: &status})
req := httptest.NewRequest(http.MethodPatch, "/users/AKIA-1", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d", resp.StatusCode)
}
}
func TestUpdateUser_ExplicitExpiration(t *testing.T) {
app, admin := newUsersTestApp(t)
wantTime, _ := time.Parse(time.RFC3339, "2030-01-02T03:04:05Z")
admin.UpdateKeyFn = func(_ context.Context, _ string, req models.UpdateKeyRequest) (*models.GarageKeyInfo, error) {
if req.Expiration == nil || !req.Expiration.Equal(wantTime) {
t.Errorf("Expiration = %v, want %v", req.Expiration, wantTime)
}
if req.NeverExpires {
t.Error("NeverExpires should be false with explicit expiration")
}
return &models.GarageKeyInfo{}, nil
}
exp := "2030-01-02T03:04:05Z"
body, _ := json.Marshal(models.UpdateUserRequest{Expiration: &exp})
req := httptest.NewRequest(http.MethodPatch, "/users/AKIA-1", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d", resp.StatusCode)
}
}
func TestUpdateUser_BadExpirationFormat400(t *testing.T) {
app, _ := newUsersTestApp(t)
exp := "not-a-date"
body, _ := json.Marshal(models.UpdateUserRequest{Expiration: &exp})
req := httptest.NewRequest(http.MethodPatch, "/users/AKIA-1", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("status = %d, want 400", resp.StatusCode)
}
}
func TestUpdateUser_MalformedJSON400(t *testing.T) {
app, _ := newUsersTestApp(t)
req := httptest.NewRequest(http.MethodPatch, "/users/AKIA-1", strings.NewReader("{not-json"))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("status = %d, want 400", resp.StatusCode)
}
}
func TestUpdateUser_AdminError500(t *testing.T) {
app, admin := newUsersTestApp(t)
admin.UpdateKeyFn = func(_ context.Context, _ string, _ models.UpdateKeyRequest) (*models.GarageKeyInfo, error) {
return nil, errors.New("boom")
}
status := "active"
body, _ := json.Marshal(models.UpdateUserRequest{Status: &status})
req := httptest.NewRequest(http.MethodPatch, "/users/AKIA-1", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusInternalServerError {
t.Fatalf("status = %d, want 500", resp.StatusCode)
}
}