package middleware import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" "github.com/dhao2001/mygo/internal/model" "github.com/dhao2001/mygo/internal/service" ) type stubAccessAuthenticator struct { wantToken string principal service.Principal err error called bool } func (s *stubAccessAuthenticator) AuthenticateAccessToken(_ context.Context, token string) (*service.Principal, error) { s.called = true if s.wantToken != "" && token != s.wantToken { return nil, model.NewUnauthenticatedError("invalid token") } if s.err != nil { return nil, s.err } return &s.principal, nil } func setupTestRouter(authenticator accessAuthenticator) *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() r.Use(AuthRequired(authenticator)) r.GET("/protected", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "user_id": MustGetUserID(c), "is_admin": MustGetPrincipal(c).IsAdmin, }) }) return r } func TestAuthRequiredNoHeader(t *testing.T) { authenticator := &stubAccessAuthenticator{} r := setupTestRouter(authenticator) req := httptest.NewRequest(http.MethodGet, "/protected", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Errorf("status = %d, want %d", rec.Code, http.StatusUnauthorized) } if authenticator.called { t.Fatal("authenticator should not be called without a bearer token") } } func TestAuthRequiredInvalidFormat(t *testing.T) { authenticator := &stubAccessAuthenticator{} r := setupTestRouter(authenticator) req := httptest.NewRequest(http.MethodGet, "/protected", nil) req.Header.Set("Authorization", "invalid") rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Errorf("status = %d, want %d", rec.Code, http.StatusUnauthorized) } if authenticator.called { t.Fatal("authenticator should not be called for malformed authorization header") } } func TestAuthRequiredNotBearer(t *testing.T) { authenticator := &stubAccessAuthenticator{} r := setupTestRouter(authenticator) req := httptest.NewRequest(http.MethodGet, "/protected", nil) req.Header.Set("Authorization", "Basic dXNlcjpwYXNz") rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Errorf("status = %d, want %d", rec.Code, http.StatusUnauthorized) } if authenticator.called { t.Fatal("authenticator should not be called for non-bearer authorization") } } func TestAuthRequiredServiceRejectsToken(t *testing.T) { authenticator := &stubAccessAuthenticator{err: model.NewUnauthenticatedError("invalid or expired token")} r := setupTestRouter(authenticator) req := httptest.NewRequest(http.MethodGet, "/protected", nil) req.Header.Set("Authorization", "Bearer expired") rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Errorf("status = %d, want %d", rec.Code, http.StatusUnauthorized) } if !authenticator.called { t.Fatal("authenticator was not called") } } func TestAuthRequiredValidToken(t *testing.T) { authenticator := &stubAccessAuthenticator{ wantToken: "valid", principal: service.Principal{ UserID: "user-1", IsAdmin: true, }, } r := setupTestRouter(authenticator) req := httptest.NewRequest(http.MethodGet, "/protected", nil) req.Header.Set("Authorization", "Bearer valid") rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("status = %d, want %d", rec.Code, http.StatusOK) } if !strings.Contains(rec.Body.String(), "user-1") { t.Errorf("response body %q does not contain user id", rec.Body.String()) } if !strings.Contains(rec.Body.String(), "true") { t.Errorf("response body %q does not contain admin flag", rec.Body.String()) } } func TestMustGetUserIDPanics(t *testing.T) { gin.SetMode(gin.TestMode) r := gin.New() r.GET("/naked", func(c *gin.Context) { MustGetUserID(c) }) req := httptest.NewRequest(http.MethodGet, "/naked", nil) rec := httptest.NewRecorder() defer func() { if r := recover(); r == nil { t.Error("expected panic, got none") } }() r.ServeHTTP(rec, req) } type errorBody struct { Error struct { Message string `json:"message"` } `json:"error"` } func injectPrincipalMiddleware(principal *service.Principal) gin.HandlerFunc { return func(c *gin.Context) { if principal != nil { c.Set(principalKey, *principal) c.Set(userIDKey, principal.UserID) } c.Next() } } func TestAdminRequired_AdminPasses(t *testing.T) { gin.SetMode(gin.TestMode) r := gin.New() r.Use(injectPrincipalMiddleware(&service.Principal{UserID: "admin-1", IsAdmin: true})) r.Use(AdminRequired()) r.GET("/admin", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) req := httptest.NewRequest(http.MethodGet, "/admin", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("status = %d, want %d", rec.Code, http.StatusOK) } } func TestAdminRequired_NonAdminForbidden(t *testing.T) { gin.SetMode(gin.TestMode) r := gin.New() r.Use(injectPrincipalMiddleware(&service.Principal{UserID: "user-1", IsAdmin: false})) r.Use(AdminRequired()) r.GET("/admin", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) req := httptest.NewRequest(http.MethodGet, "/admin", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusForbidden { t.Errorf("status = %d, want %d", rec.Code, http.StatusForbidden) } var body errorBody if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("unmarshal: %v", err) } if body.Error.Message != "admin access required" { t.Errorf("message = %q, want %q", body.Error.Message, "admin access required") } } func TestAdminRequired_NoPrincipal(t *testing.T) { gin.SetMode(gin.TestMode) r := gin.New() r.Use(AdminRequired()) r.GET("/admin", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) req := httptest.NewRequest(http.MethodGet, "/admin", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Errorf("status = %d, want %d", rec.Code, http.StatusUnauthorized) } }