cambios para worker-remoto
This commit is contained in:
parent
98e0cd0e46
commit
be56de7dd4
19 changed files with 1544 additions and 216 deletions
55
backend/internal/auth/jwt.go
Normal file
55
backend/internal/auth/jwt.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var jwtSecret []byte
|
||||
|
||||
func SetJWTSecret(secret string) {
|
||||
jwtSecret = []byte(secret)
|
||||
}
|
||||
|
||||
func GetJWTSecret() []byte {
|
||||
return jwtSecret
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GenerateToken(userID int64, email, username string, isAdmin bool) (string, error) {
|
||||
expirationTime := time.Now().Add(24 * time.Hour)
|
||||
claims := &Claims{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
Username: username,
|
||||
IsAdmin: isAdmin,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(jwtSecret)
|
||||
}
|
||||
|
||||
func ValidateToken(tokenString string) (*Claims, error) {
|
||||
claims := &Claims{}
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
return jwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
96
backend/internal/auth/jwt_test.go
Normal file
96
backend/internal/auth/jwt_test.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGenerateAndValidateToken(t *testing.T) {
|
||||
SetJWTSecret("test-secret-key-12345")
|
||||
|
||||
token, err := GenerateToken(1, "test@example.com", "testuser", true)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateToken failed: %v", err)
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
t.Fatal("token should not be empty")
|
||||
}
|
||||
|
||||
claims, err := ValidateToken(token)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateToken failed: %v", err)
|
||||
}
|
||||
|
||||
if claims.UserID != 1 {
|
||||
t.Errorf("expected userID 1, got %d", claims.UserID)
|
||||
}
|
||||
if claims.Email != "test@example.com" {
|
||||
t.Errorf("expected email test@example.com, got %s", claims.Email)
|
||||
}
|
||||
if claims.Username != "testuser" {
|
||||
t.Errorf("expected username testuser, got %s", claims.Username)
|
||||
}
|
||||
if !claims.IsAdmin {
|
||||
t.Error("expected isAdmin true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateInvalidToken(t *testing.T) {
|
||||
SetJWTSecret("test-secret-key-12345")
|
||||
|
||||
_, err := ValidateToken("invalid-token")
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTokenWithWrongSecret(t *testing.T) {
|
||||
SetJWTSecret("secret-1")
|
||||
|
||||
token, err := GenerateToken(1, "test@example.com", "testuser", false)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateToken failed: %v", err)
|
||||
}
|
||||
|
||||
SetJWTSecret("secret-2")
|
||||
|
||||
_, err = ValidateToken(token)
|
||||
if err == nil {
|
||||
t.Error("expected error for token with different secret")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimsHaveExpiration(t *testing.T) {
|
||||
SetJWTSecret("test-secret-key-12345")
|
||||
|
||||
token, err := GenerateToken(1, "test@example.com", "testuser", false)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateToken failed: %v", err)
|
||||
}
|
||||
|
||||
claims, err := ValidateToken(token)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateToken failed: %v", err)
|
||||
}
|
||||
|
||||
if claims.ExpiresAt == nil {
|
||||
t.Error("expected expiration time to be set")
|
||||
}
|
||||
|
||||
expTime := claims.ExpiresAt.Time
|
||||
expectedExpiration := time.Now().Add(24 * time.Hour)
|
||||
|
||||
if expTime.Sub(expectedExpiration) > time.Minute {
|
||||
t.Errorf("expiration time should be ~24 hours from now, got %v", expTime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyToken(t *testing.T) {
|
||||
SetJWTSecret("test-secret-key-12345")
|
||||
|
||||
_, err := ValidateToken("")
|
||||
if err == nil {
|
||||
t.Error("expected error for empty token")
|
||||
}
|
||||
}
|
||||
145
backend/internal/config/config_test.go
Normal file
145
backend/internal/config/config_test.go
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLoadDefaults(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
cfg := Load()
|
||||
|
||||
if cfg.ServerPort != "8080" {
|
||||
t.Errorf("expected ServerPort '8080', got %s", cfg.ServerPort)
|
||||
}
|
||||
if cfg.SecretKey != "change-this-secret-key" {
|
||||
t.Errorf("expected SecretKey 'change-this-secret-key', got %s", cfg.SecretKey)
|
||||
}
|
||||
if cfg.DefaultLang != "es" {
|
||||
t.Errorf("expected DefaultLang 'es', got %s", cfg.DefaultLang)
|
||||
}
|
||||
if cfg.NewsPerPage != 30 {
|
||||
t.Errorf("expected NewsPerPage 30, got %d", cfg.NewsPerPage)
|
||||
}
|
||||
if cfg.DockerComposeDir != "/datos/rss2" {
|
||||
t.Errorf("expected DockerComposeDir '/datos/rss2', got %s", cfg.DockerComposeDir)
|
||||
}
|
||||
if cfg.WikiImagesPath != "/app/data/wiki_images" {
|
||||
t.Errorf("expected WikiImagesPath '/app/data/wiki_images', got %s", cfg.WikiImagesPath)
|
||||
}
|
||||
if cfg.AllowedOrigins != "*" {
|
||||
t.Errorf("expected AllowedOrigins '*', got %s", cfg.AllowedOrigins)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromEnv(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("SERVER_PORT", "9000")
|
||||
os.Setenv("DATABASE_URL", "postgres://user:pass@host:5432/db")
|
||||
os.Setenv("SECRET_KEY", "my-secret-key")
|
||||
os.Setenv("DEFAULT_LANG", "en")
|
||||
os.Setenv("NEWS_PER_PAGE", "50")
|
||||
os.Setenv("JWT_EXPIRATION", "48h")
|
||||
os.Setenv("RATE_LIMIT_PER_MINUTE", "100")
|
||||
os.Setenv("DOCKER_COMPOSE_DIR", "/custom/path")
|
||||
os.Setenv("WIKI_IMAGES_PATH", "/custom/images")
|
||||
|
||||
cfg := Load()
|
||||
|
||||
if cfg.ServerPort != "9000" {
|
||||
t.Errorf("expected ServerPort '9000', got %s", cfg.ServerPort)
|
||||
}
|
||||
if cfg.DatabaseURL != "postgres://user:pass@host:5432/db" {
|
||||
t.Errorf("expected DatabaseURL, got %s", cfg.DatabaseURL)
|
||||
}
|
||||
if cfg.SecretKey != "my-secret-key" {
|
||||
t.Errorf("expected SecretKey 'my-secret-key', got %s", cfg.SecretKey)
|
||||
}
|
||||
if cfg.DefaultLang != "en" {
|
||||
t.Errorf("expected DefaultLang 'en', got %s", cfg.DefaultLang)
|
||||
}
|
||||
if cfg.NewsPerPage != 50 {
|
||||
t.Errorf("expected NewsPerPage 50, got %d", cfg.NewsPerPage)
|
||||
}
|
||||
if cfg.JWTExpiration != 48*time.Hour {
|
||||
t.Errorf("expected JWTExpiration 48h, got %v", cfg.JWTExpiration)
|
||||
}
|
||||
if cfg.RateLimitPerMinute != 100 {
|
||||
t.Errorf("expected RateLimitPerMinute 100, got %d", cfg.RateLimitPerMinute)
|
||||
}
|
||||
if cfg.DockerComposeDir != "/custom/path" {
|
||||
t.Errorf("expected DockerComposeDir '/custom/path', got %s", cfg.DockerComposeDir)
|
||||
}
|
||||
if cfg.WikiImagesPath != "/custom/images" {
|
||||
t.Errorf("expected WikiImagesPath '/custom/images', got %s", cfg.WikiImagesPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadInvalidInt(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("NEWS_PER_PAGE", "invalid")
|
||||
|
||||
cfg := Load()
|
||||
|
||||
if cfg.NewsPerPage != 30 {
|
||||
t.Errorf("expected default NewsPerPage 30 for invalid value, got %d", cfg.NewsPerPage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadInvalidDuration(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("JWT_EXPIRATION", "invalid")
|
||||
|
||||
cfg := Load()
|
||||
|
||||
if cfg.JWTExpiration != 24*time.Hour {
|
||||
t.Errorf("expected default JWTExpiration 24h for invalid value, got %v", cfg.JWTExpiration)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEnv(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("TEST_KEY", "test_value")
|
||||
|
||||
result := getEnv("TEST_KEY", "default")
|
||||
if result != "test_value" {
|
||||
t.Errorf("expected 'test_value', got %s", result)
|
||||
}
|
||||
|
||||
result = getEnv("NON_EXISTENT", "default")
|
||||
if result != "default" {
|
||||
t.Errorf("expected 'default', got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEnvInt(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("TEST_INT", "123")
|
||||
|
||||
result := getEnvInt("TEST_INT", 0)
|
||||
if result != 123 {
|
||||
t.Errorf("expected 123, got %d", result)
|
||||
}
|
||||
|
||||
result = getEnvInt("NON_EXISTENT", 99)
|
||||
if result != 99 {
|
||||
t.Errorf("expected 99, got %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEnvDuration(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("TEST_DURATION", "1h30m")
|
||||
|
||||
result := getEnvDuration("TEST_DURATION", 0)
|
||||
if result != 90*time.Minute {
|
||||
t.Errorf("expected 90m, got %v", result)
|
||||
}
|
||||
|
||||
result = getEnvDuration("NON_EXISTENT", 5*time.Minute)
|
||||
if result != 5*time.Minute {
|
||||
t.Errorf("expected 5m, got %v", result)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"context"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
|
@ -14,6 +15,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rss2/backend/internal/config"
|
||||
"github.com/rss2/backend/internal/db"
|
||||
"github.com/rss2/backend/internal/models"
|
||||
)
|
||||
|
|
@ -465,13 +467,14 @@ func StartWorkers(c *gin.Context) {
|
|||
}
|
||||
|
||||
// Detener cualquier translator existente
|
||||
composeDir := config.Load().DockerComposeDir
|
||||
stopCmd := exec.Command("docker", "compose", "stop", "translator", "translator-gpu")
|
||||
stopCmd.Dir = "/datos/rss2"
|
||||
stopCmd.Dir = composeDir
|
||||
stopCmd.Run()
|
||||
|
||||
// Iniciar con el número de workers
|
||||
startCmd := exec.Command("docker", "compose", "up", "-d", "--scale", fmt.Sprintf("%s=%d", serviceName, workers), serviceName)
|
||||
startCmd.Dir = "/datos/rss2"
|
||||
startCmd.Dir = composeDir
|
||||
output, err := startCmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -483,9 +486,15 @@ func StartWorkers(c *gin.Context) {
|
|||
}
|
||||
|
||||
// Actualizar estado en BD
|
||||
db.GetPool().Exec(ctx, "UPDATE config SET value = 'running', updated_at = NOW() WHERE key = 'translator_status'")
|
||||
db.GetPool().Exec(ctx, "UPDATE config SET value = $1, updated_at = NOW() WHERE key = 'translator_type'", translatorType)
|
||||
db.GetPool().Exec(ctx, "UPDATE config SET value = $1, updated_at = NOW() WHERE key = 'translator_workers'", translatorWorkers)
|
||||
if _, err := db.GetPool().Exec(ctx, "UPDATE config SET value = 'running', updated_at = NOW() WHERE key = 'translator_status'"); err != nil {
|
||||
log.Printf("Failed to update translator_status: %v", err)
|
||||
}
|
||||
if _, err := db.GetPool().Exec(ctx, "UPDATE config SET value = $1, updated_at = NOW() WHERE key = 'translator_type'", translatorType); err != nil {
|
||||
log.Printf("Failed to update translator_type: %v", err)
|
||||
}
|
||||
if _, err := db.GetPool().Exec(ctx, "UPDATE config SET value = $1, updated_at = NOW() WHERE key = 'translator_workers'", translatorWorkers); err != nil {
|
||||
log.Printf("Failed to update translator_workers: %v", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Workers started successfully",
|
||||
|
|
@ -497,8 +506,9 @@ func StartWorkers(c *gin.Context) {
|
|||
|
||||
func StopWorkers(c *gin.Context) {
|
||||
// Detener traductores
|
||||
composeDir := config.Load().DockerComposeDir
|
||||
cmd := exec.Command("docker", "compose", "stop", "translator", "translator-gpu")
|
||||
cmd.Dir = "/datos/rss2"
|
||||
cmd.Dir = composeDir
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -510,7 +520,9 @@ func StopWorkers(c *gin.Context) {
|
|||
}
|
||||
|
||||
// Actualizar estado en BD
|
||||
db.GetPool().Exec(c.Request.Context(), "UPDATE config SET value = 'stopped', updated_at = NOW() WHERE key = 'translator_status'")
|
||||
if _, err := db.GetPool().Exec(c.Request.Context(), "UPDATE config SET value = 'stopped', updated_at = NOW() WHERE key = 'translator_status'"); err != nil {
|
||||
log.Printf("Failed to update translator_status: %v", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Workers stopped successfully",
|
||||
|
|
|
|||
|
|
@ -2,18 +2,14 @@ package handlers
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/rss2/backend/internal/config"
|
||||
"github.com/rss2/backend/internal/auth"
|
||||
"github.com/rss2/backend/internal/db"
|
||||
"github.com/rss2/backend/internal/models"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var jwtSecret []byte
|
||||
|
||||
func CheckFirstUser(c *gin.Context) {
|
||||
var count int
|
||||
err := db.GetPool().QueryRow(c.Request.Context(), "SELECT COUNT(*) FROM users").Scan(&count)
|
||||
|
|
@ -24,18 +20,6 @@ func CheckFirstUser(c *gin.Context) {
|
|||
c.JSON(http.StatusOK, gin.H{"is_first_user": count == 0, "total_users": count})
|
||||
}
|
||||
|
||||
func InitAuth(secret string) {
|
||||
jwtSecret = []byte(secret)
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func Login(c *gin.Context) {
|
||||
var req models.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
|
@ -60,20 +44,7 @@ func Login(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
expirationTime := time.Now().Add(24 * time.Hour)
|
||||
claims := &Claims{
|
||||
UserID: user.ID,
|
||||
Email: user.Email,
|
||||
Username: user.Username,
|
||||
IsAdmin: user.IsAdmin,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString(jwtSecret)
|
||||
tokenString, err := auth.GenerateToken(user.ID, user.Email, user.Username, user.IsAdmin)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to generate token"})
|
||||
return
|
||||
|
|
@ -127,20 +98,7 @@ func Register(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
expirationTime := time.Now().Add(24 * time.Hour)
|
||||
claims := &Claims{
|
||||
UserID: user.ID,
|
||||
Email: user.Email,
|
||||
Username: user.Username,
|
||||
IsAdmin: user.IsAdmin,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString(jwtSecret)
|
||||
tokenString, err := auth.GenerateToken(user.ID, user.Email, user.Username, user.IsAdmin)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to generate token"})
|
||||
return
|
||||
|
|
@ -160,7 +118,7 @@ func GetCurrentUser(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
claims := userVal.(*Claims)
|
||||
claims := userVal.(*auth.Claims)
|
||||
|
||||
var user models.User
|
||||
err := db.GetPool().QueryRow(c.Request.Context(), `
|
||||
|
|
@ -176,8 +134,3 @@ func GetCurrentUser(c *gin.Context) {
|
|||
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
func init() {
|
||||
cfg := config.Load()
|
||||
InitAuth(cfg.SecretKey)
|
||||
}
|
||||
|
|
|
|||
108
backend/internal/handlers/auth_test.go
Normal file
108
backend/internal/handlers/auth_test.go
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rss2/backend/internal/auth"
|
||||
"github.com/rss2/backend/internal/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gin.SetMode(gin.TestMode)
|
||||
auth.SetJWTSecret("test-secret-key-12345")
|
||||
}
|
||||
|
||||
func TestLoginInvalidRequest(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.POST("/auth/login", Login)
|
||||
|
||||
body := []byte(`{}`)
|
||||
req, _ := http.NewRequest("POST", "/auth/login", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp models.ErrorResponse
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
if resp.Error != "Invalid request" {
|
||||
t.Errorf("expected 'Invalid request', got %s", resp.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginInvalidCredentials(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.POST("/auth/login", Login)
|
||||
|
||||
body := []byte(`{"email":"invalid@test.com","password":"wrongpass"}`)
|
||||
req, _ := http.NewRequest("POST", "/auth/login", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected status 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterInvalidRequest(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.POST("/auth/register", Register)
|
||||
|
||||
body := []byte(`{}`)
|
||||
req, _ := http.NewRequest("POST", "/auth/register", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckFirstUser(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.GET("/auth/check-first-user", CheckFirstUser)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/auth/check-first-user", nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
if _, ok := resp["is_first_user"]; !ok {
|
||||
t.Error("expected is_first_user in response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCurrentUserUnauthorized(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.GET("/auth/me", GetCurrentUser)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/auth/me", nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected status 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
141
backend/internal/middleware/auth_test.go
Normal file
141
backend/internal/middleware/auth_test.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rss2/backend/internal/auth"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gin.SetMode(gin.TestMode)
|
||||
auth.SetJWTSecret("test-secret-key-12345")
|
||||
}
|
||||
|
||||
func TestAuthRequiredNoHeader(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.Use(AuthRequired())
|
||||
router.GET("/protected", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("GET", "/protected", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected status 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthRequiredInvalidToken(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.Use(AuthRequired())
|
||||
router.GET("/protected", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("GET", "/protected", nil)
|
||||
req.Header.Set("Authorization", "Bearer invalid-token")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected status 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthRequiredValidToken(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.Use(AuthRequired())
|
||||
router.GET("/protected", func(c *gin.Context) {
|
||||
userVal, _ := c.Get("user")
|
||||
claims := userVal.(*auth.Claims)
|
||||
c.JSON(200, gin.H{"user_id": claims.UserID})
|
||||
})
|
||||
|
||||
token, _ := auth.GenerateToken(42, "test@test.com", "testuser", false)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/protected", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminRequiredNotAdmin(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.Use(AuthRequired())
|
||||
router.Use(AdminRequired())
|
||||
router.GET("/admin", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
token, _ := auth.GenerateToken(1, "test@test.com", "testuser", false)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/admin", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected status 403, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminRequiredIsAdmin(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.Use(AuthRequired())
|
||||
router.Use(AdminRequired())
|
||||
router.GET("/admin", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
token, _ := auth.GenerateToken(1, "test@test.com", "adminuser", true)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/admin", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSMiddleware(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.Use(CORSMiddleware())
|
||||
router.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Header().Get("Access-Control-Allow-Origin") == "" {
|
||||
t.Error("expected CORS header to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSPreflight(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.Use(CORSMiddleware())
|
||||
router.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("OPTIONS", "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Errorf("expected status 204 for preflight, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue