package router import ( "bufio" "bytes" "encoding/json" "net" "net/http" "net/http/httptest" "strconv" "strings" "testing" "github.com/gin-gonic/gin" "gorm.io/driver/sqlite" "gorm.io/gorm" "goaresv3/app/accounts/models" "goaresv3/config" jwtHelper "goaresv3/pkg/jwt" ) func setupRouterTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open("file:router_test?mode=memory&cache=shared"), &gorm.Config{}) if err != nil { t.Fatalf("failed to open sqlite db: %v", err) } if err := db.AutoMigrate(&models.User{}, &models.SocialAccount{}, &models.Profile{}); err != nil { t.Fatalf("failed to migrate models: %v", err) } config.DB = db return db } func startFakeSMTP(t *testing.T) (host, port string, done chan error) { t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("failed to start fake smtp: %v", err) } done = make(chan error, 1) go func() { defer ln.Close() conn, err := ln.Accept() if err != nil { done <- err return } defer conn.Close() r := bufio.NewReader(conn) w := bufio.NewWriter(conn) write := func(s string) error { if _, err := w.WriteString(s); err != nil { return err } return w.Flush() } if err := write("220 localhost SMTP ready\r\n"); err != nil { done <- err return } for { line, err := r.ReadString('\n') if err != nil { done <- err return } cmd := strings.TrimSpace(line) switch { case strings.HasPrefix(strings.ToUpper(cmd), "EHLO") || strings.HasPrefix(strings.ToUpper(cmd), "HELO"): if err := write("250-localhost\r\n250 AUTH PLAIN\r\n"); err != nil { done <- err return } case strings.HasPrefix(strings.ToUpper(cmd), "MAIL FROM"): if err := write("250 OK\r\n"); err != nil { done <- err return } case strings.HasPrefix(strings.ToUpper(cmd), "RCPT TO"): if err := write("250 OK\r\n"); err != nil { done <- err return } case strings.HasPrefix(strings.ToUpper(cmd), "DATA"): if err := write("354 End data with .\r\n"); err != nil { done <- err return } for { d, err := r.ReadString('\n') if err != nil { done <- err return } if strings.TrimSpace(d) == "." { break } } if err := write("250 OK queued\r\n"); err != nil { done <- err return } case strings.HasPrefix(strings.ToUpper(cmd), "QUIT"): _ = write("221 Bye\r\n") done <- nil return default: if err := write("250 OK\r\n"); err != nil { done <- err return } } } }() h, p, err := net.SplitHostPort(ln.Addr().String()) if err != nil { t.Fatalf("failed to parse fake smtp address: %v", err) } if _, err := strconv.Atoi(p); err != nil { t.Fatalf("invalid fake smtp port: %v", err) } return h, p, done } func TestAllEndpointsFlow(t *testing.T) { gin.SetMode(gin.TestMode) db := setupRouterTestDB(t) host, port, smtpDone := startFakeSMTP(t) t.Setenv("JWT_SECRET", "test-secret-1234567890") t.Setenv("JWT_REFRESH_SECRET", "test-refresh-secret-1234567890") t.Setenv("APP_BASE_URL", "http://localhost:8080") t.Setenv("EMAIL_HOST", host) t.Setenv("EMAIL_PORT", port) t.Setenv("EMAIL_HOST_USER", "") t.Setenv("EMAIL_HOST_PASSWORD", "") t.Setenv("EMAIL_USE_TLS", "false") t.Setenv("EMAIL_USE_SSL", "false") t.Setenv("EMAIL_FROM", "noreply@example.com") r := gin.New() Setup(r) regBody := []byte(`{"username":"ali","email":"ali@example.com","password":"password123","confirm_password":"password123"}`) regReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewReader(regBody)) regReq.Header.Set("Content-Type", "application/json") regResp := httptest.NewRecorder() r.ServeHTTP(regResp, regReq) if regResp.Code != http.StatusCreated { t.Fatalf("register expected 201, got %d body=%s", regResp.Code, regResp.Body.String()) } if err := <-smtpDone; err != nil { t.Fatalf("fake smtp ended with error: %v", err) } invalidVerifyReq := httptest.NewRequest(http.MethodGet, "/api/v1/auth/verify-email?token=bad-token", nil) invalidVerifyResp := httptest.NewRecorder() r.ServeHTTP(invalidVerifyResp, invalidVerifyReq) if invalidVerifyResp.Code != http.StatusBadRequest { t.Fatalf("invalid verify expected 400, got %d", invalidVerifyResp.Code) } loginBody := []byte(`{"email":"ali@example.com","password":"password123"}`) loginBeforeReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(loginBody)) loginBeforeReq.Header.Set("Content-Type", "application/json") loginBeforeResp := httptest.NewRecorder() r.ServeHTTP(loginBeforeResp, loginBeforeReq) if loginBeforeResp.Code != http.StatusForbidden { t.Fatalf("login before verify expected 403, got %d", loginBeforeResp.Code) } var user models.User if err := db.Where("email = ?", "ali@example.com").First(&user).Error; err != nil { t.Fatalf("failed to fetch registered user: %v", err) } if user.EmailVerifyToken == "" { t.Fatal("expected email verify token to be set") } verifyReq := httptest.NewRequest(http.MethodGet, "/api/v1/auth/verify-email?token="+user.EmailVerifyToken, nil) verifyResp := httptest.NewRecorder() r.ServeHTTP(verifyResp, verifyReq) if verifyResp.Code != http.StatusOK { t.Fatalf("verify expected 200, got %d", verifyResp.Code) } loginReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(loginBody)) loginReq.Header.Set("Content-Type", "application/json") loginResp := httptest.NewRecorder() r.ServeHTTP(loginResp, loginReq) if loginResp.Code != http.StatusOK { t.Fatalf("login expected 200, got %d body=%s", loginResp.Code, loginResp.Body.String()) } var loginJSON map[string]any if err := json.Unmarshal(loginResp.Body.Bytes(), &loginJSON); err != nil { t.Fatalf("failed to decode login response: %v", err) } accessToken, _ := loginJSON["access_token"].(string) refreshToken, _ := loginJSON["refresh_token"].(string) if accessToken == "" || refreshToken == "" { t.Fatal("expected access_token and refresh_token in login response") } refreshBody := []byte(`{"refresh_token":"` + refreshToken + `"}`) refreshReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/refresh", bytes.NewReader(refreshBody)) refreshReq.Header.Set("Content-Type", "application/json") refreshResp := httptest.NewRecorder() r.ServeHTTP(refreshResp, refreshReq) if refreshResp.Code != http.StatusOK { t.Fatalf("refresh expected 200, got %d body=%s", refreshResp.Code, refreshResp.Body.String()) } meReq := httptest.NewRequest(http.MethodGet, "/api/v1/me", nil) meReq.Header.Set("Authorization", "Bearer "+accessToken) meResp := httptest.NewRecorder() r.ServeHTTP(meResp, meReq) if meResp.Code != http.StatusOK { t.Fatalf("me expected 200, got %d body=%s", meResp.Code, meResp.Body.String()) } var meJSON map[string]any if err := json.Unmarshal(meResp.Body.Bytes(), &meJSON); err != nil { t.Fatalf("failed to decode me response: %v", err) } if _, ok := meJSON["username"]; !ok { t.Fatal("expected username in /api/v1/me response") } swaggerReq := httptest.NewRequest(http.MethodGet, "/swagger/index.html", nil) swaggerResp := httptest.NewRecorder() r.ServeHTTP(swaggerResp, swaggerReq) if swaggerResp.Code != http.StatusOK { t.Fatalf("swagger index expected 200, got %d", swaggerResp.Code) } initializerReq := httptest.NewRequest(http.MethodGet, "/swagger/swagger-initializer.js", nil) initializerResp := httptest.NewRecorder() r.ServeHTTP(initializerResp, initializerReq) if initializerResp.Code != http.StatusOK { t.Fatalf("swagger initializer expected 200, got %d", initializerResp.Code) } if !strings.Contains(initializerResp.Body.String(), "requestInterceptor") { t.Fatal("expected requestInterceptor in swagger initializer") } } func TestProtectedEndpointWithoutAuthHeader(t *testing.T) { gin.SetMode(gin.TestMode) setupRouterTestDB(t) t.Setenv("JWT_SECRET", "test-secret-1234567890") r := gin.New() Setup(r) req := httptest.NewRequest(http.MethodGet, "/api/v1/me", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", w.Code) } } func TestProtectedEndpointWithRawTokenRejected(t *testing.T) { gin.SetMode(gin.TestMode) setupRouterTestDB(t) t.Setenv("JWT_SECRET", "test-secret-1234567890") t.Setenv("JWT_REFRESH_SECRET", "test-refresh-secret-1234567890") verified := true user := models.User{UserName: "raw", Email: "raw@example.com", EmailVerified: &verified} if err := config.DB.Create(&user).Error; err != nil { t.Fatalf("failed to seed user: %v", err) } access, err := jwtHelper.GenerateAccessToken(user.ID, user.Email, user.UserName) if err != nil { t.Fatalf("failed to generate access token: %v", err) } r := gin.New() Setup(r) req := httptest.NewRequest(http.MethodGet, "/api/v1/me", nil) req.Header.Set("Authorization", access) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", w.Code) } } func TestSwaggerDisallowsNonGET(t *testing.T) { gin.SetMode(gin.TestMode) setupRouterTestDB(t) r := gin.New() Setup(r) req := httptest.NewRequest(http.MethodPost, "/swagger/index.html", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", w.Code) } }