fix: add missing get handler for the current user

This commit is contained in:
ae 2025-04-02 22:13:01 +03:00
parent 15c4666ace
commit 7646df76df
Signed by: ae
GPG Key ID: 995EFD5C1B532B3E
2 changed files with 131 additions and 17 deletions

View File

@ -22,9 +22,9 @@ type userCtxKey struct{}
type userResponse struct {
ID uuid.UUID `json:"id"`
Username string `json:"username"`
IsAdmin bool `json:"is_admin"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
IsAdmin bool `json:"is_admin"`
}
// Mockable database operations interface
@ -54,6 +54,8 @@ func (rs usersResource) Routes() chi.Router {
r.Group(func(r chi.Router) {
r.Use(requireAccessToken(rs.JWTSecret))
r.Get("/me", rs.Get) // GET /users/me - get current user data
// Admin only general routes
r.Group(func(r chi.Router) {
r.Use(adminOnlyMiddleware)
@ -67,7 +69,6 @@ func (rs usersResource) Routes() chi.Router {
// Admin routes
r.Route("/admin", func(r chi.Router) {
r.Use(adminOnlyMiddleware)
r.Get("/", rs.Get) // GET /users/admin/{id} - get single user
r.Delete("/", rs.AdminDelete) // DELETE /users/admin/{id} - delete user
})
@ -181,9 +182,9 @@ func (rs usersResource) Login(w http.ResponseWriter, r *http.Request) {
response["user"] = userResponse{
ID: user.ID,
Username: user.Username,
IsAdmin: user.IsAdmin,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
IsAdmin: user.IsAdmin,
}
}
@ -212,17 +213,30 @@ func (rs usersResource) List(w http.ResponseWriter, r *http.Request) {
}
func (rs usersResource) Get(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(userCtxKey{}).(data.User)
claims, ok := r.Context().Value(userCtxKey{}).(*userClaims)
if !ok {
respondError(w, http.StatusUnauthorized, "Unauthorized")
return
}
userID, err := uuid.Parse(claims.Subject)
if err != nil {
respondError(w, http.StatusInternalServerError, "Invalid user ID")
return
}
user, err := rs.Users.GetUserByID(r.Context(), userID)
if err != nil {
respondError(w, http.StatusNotFound, "User not found")
return
}
respondJSON(w, http.StatusOK, map[string]any{
"id": user.ID,
"username": user.Username,
"created_at": user.CreatedAt,
"updated_at": user.UpdatedAt,
respondJSON(w, http.StatusOK, userResponse{
ID: user.ID,
Username: user.Username,
IsAdmin: user.IsAdmin,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
})
}

View File

@ -11,6 +11,7 @@ import (
"net/url"
"strings"
"testing"
"time"
"git.umbrella.haus/ae/notatest/pkg/data"
"github.com/golang-jwt/jwt/v5"
@ -244,17 +245,116 @@ func TestOwnerDelete_InvalidCredentials(t *testing.T) {
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
func TestGetUser_NotFound(t *testing.T) {
mockStore := &mockUserStore{}
rs := usersResource{Users: mockStore}
func TestUsersGetCurrentUser(t *testing.T) {
validUserID := uuid.New()
testTime := time.Now().UTC().Truncate(time.Second)
testUser := data.User{
ID: validUserID,
Username: "testuser",
CreatedAt: &testTime,
UpdatedAt: &testTime,
IsAdmin: false,
}
// No user in context
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
tests := []struct {
name string
setupContext func(context.Context) context.Context
mockSetup func(*mockUserStore)
wantStatus int
wantResponse string
}{
{
name: "success",
setupContext: func(ctx context.Context) context.Context {
return context.WithValue(ctx, userCtxKey{}, &userClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: validUserID.String(),
},
})
},
mockSetup: func(m *mockUserStore) {
m.GetUserByIDFunc = func(_ context.Context, id uuid.UUID) (data.User, error) {
assert.Equal(t, validUserID, id)
return testUser, nil
}
},
wantStatus: http.StatusOK,
wantResponse: fmt.Sprintf(
`{"created_at":"%s","id":"%s","is_admin":false,"updated_at":"%s","username":"testuser"}`,
testUser.CreatedAt.Format(time.RFC3339Nano),
validUserID.String(),
testUser.UpdatedAt.Format(time.RFC3339Nano),
),
},
{
name: "user not found",
setupContext: func(ctx context.Context) context.Context {
return context.WithValue(ctx, userCtxKey{}, &userClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: validUserID.String(),
},
})
},
mockSetup: func(m *mockUserStore) {
m.GetUserByIDFunc = func(_ context.Context, id uuid.UUID) (data.User, error) {
return data.User{}, errors.New("not found")
}
},
wantStatus: http.StatusNotFound,
wantResponse: `{"error":"User not found"}`,
},
{
name: "unauthorized",
setupContext: func(ctx context.Context) context.Context {
return ctx // No user claims in context
},
mockSetup: func(m *mockUserStore) {},
wantStatus: http.StatusUnauthorized,
wantResponse: `{"error":"Unauthorized"}`,
},
{
name: "invalid user ID",
setupContext: func(ctx context.Context) context.Context {
return context.WithValue(ctx, userCtxKey{}, &userClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: "invalid",
},
})
},
mockSetup: func(m *mockUserStore) {},
wantStatus: http.StatusInternalServerError,
wantResponse: `{"error":"Invalid user ID"}`,
},
}
rs.Get(w, req)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockStore := &mockUserStore{}
tt.mockSetup(mockStore)
assert.Equal(t, http.StatusNotFound, w.Code)
rs := usersResource{Users: mockStore}
req := httptest.NewRequest("GET", "/me", nil)
req = req.WithContext(tt.setupContext(req.Context()))
w := httptest.NewRecorder()
rs.Get(w, req)
assert.Equal(t, tt.wantStatus, w.Code)
if tt.wantResponse != "" {
actual := strings.TrimSpace(w.Body.String())
assert.JSONEq(t, tt.wantResponse, actual)
}
// Verify sensitive fields are never exposed
if w.Code == http.StatusOK {
var response map[string]any
json.Unmarshal(w.Body.Bytes(), &response)
_, exists := response["password_hash"]
assert.False(t, exists, "password_hash should not be exposed")
}
})
}
}
func TestUpdatePassword_DatabaseError(t *testing.T) {