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) } }