first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:40:14 +03:00
commit e04ba85564
129 changed files with 17541 additions and 0 deletions

View File

@@ -0,0 +1,735 @@
package handlers
import (
"bytes"
"encoding/base64"
"encoding/json"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"ginimageApi/app/accounts/models"
"ginimageApi/app/middleware"
"ginimageApi/configs"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func setupHandlersTestDB(t *testing.T) {
t.Helper()
t.Setenv("AVATAR_WIDTH", "64")
t.Setenv("AVATAR_HEIGHT", "64")
t.Setenv("AVATAR_QUALITY", "80")
t.Setenv("AVATAR_MAX_SIZE_MB", "5")
t.Setenv("AVATAR_FORMATS", "png")
prev := configs.DB
dsn := "file:" + t.Name() + "?mode=memory&cache=shared"
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
if err != nil {
t.Fatalf("sqlite open failed: %v", err)
}
if err := db.AutoMigrate(&models.User{}, &models.Profile{}, &models.SocialAccount{}, &models.RefreshToken{}); err != nil {
t.Fatalf("migrate failed: %v", err)
}
configs.DB = db
t.Cleanup(func() {
if sqlDB, err := db.DB(); err == nil {
_ = sqlDB.Close()
}
configs.DB = prev
})
}
func tinyPNGFixture(t *testing.T) []byte {
t.Helper()
// 1x1 PNG
const data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7Zr0kAAAAASUVORK5CYII="
b, err := base64.StdEncoding.DecodeString(data)
if err != nil {
t.Fatalf("png fixture decode failed: %v", err)
}
return b
}
func createOldAvatarFixture(t *testing.T, fileName string) (string, string) {
t.Helper()
dir := filepath.Join("uploads", "avatars")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir avatars failed: %v", err)
}
fullPath := filepath.Join(dir, fileName)
if err := os.WriteFile(fullPath, []byte("old-avatar"), 0o644); err != nil {
t.Fatalf("write old avatar failed: %v", err)
}
t.Cleanup(func() { _ = os.Remove(fullPath) })
return "/uploads/avatars/" + fileName, fullPath
}
func performJSON(r *gin.Engine, method, path string, payload any, headers map[string]string) *httptest.ResponseRecorder {
var body []byte
if payload != nil {
body, _ = json.Marshal(payload)
}
req := httptest.NewRequest(method, path, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
for k, v := range headers {
req.Header.Set(k, v)
}
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
return w
}
func performMultipart(
r *gin.Engine,
method, path string,
fields map[string]string,
fileField, fileName string,
fileContent []byte,
headers map[string]string,
) *httptest.ResponseRecorder {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
for k, v := range fields {
_ = writer.WriteField(k, v)
}
if fileField != "" {
part, _ := writer.CreateFormFile(fileField, fileName)
_, _ = part.Write(fileContent)
}
_ = writer.Close()
req := httptest.NewRequest(method, path, &body)
req.Header.Set("Content-Type", writer.FormDataContentType())
for k, v := range headers {
req.Header.Set(k, v)
}
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
return w
}
func assertJWTFormat(t *testing.T, token string) {
t.Helper()
parts := strings.Split(token, ".")
if len(parts) != 3 {
t.Fatalf("token JWT formatinda olmali, segment sayisi: %d", len(parts))
}
}
func TestAuthFlowRegisterLoginMeRefresh(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("JWT_SECRET", "test-secret")
setupHandlersTestDB(t)
r := gin.New()
r.POST("/register", Register)
r.POST("/login", Login)
r.POST("/refresh", Refresh)
r.GET("/verify-email", VerifyEmail)
r.GET("/me", middleware.AuthRequired(), Me)
registerPayload := map[string]any{
"username": "john",
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe",
"password": "secret123",
"confirm_password": "secret123",
}
wReg := performJSON(r, http.MethodPost, "/register", registerPayload, nil)
if wReg.Code != http.StatusCreated {
t.Fatalf("register expected 201, got %d body=%s", wReg.Code, wReg.Body.String())
}
var regResp map[string]any
if err := json.Unmarshal(wReg.Body.Bytes(), &regResp); err != nil {
t.Fatalf("register json parse failed: %v", err)
}
accessToken, _ := regResp["access"].(string)
if accessToken != "" {
t.Fatalf("register should not return direct access token before email verification")
}
verificationToken, _ := regResp["verification_token"].(string)
if verificationToken == "" {
t.Fatalf("verification_token must be returned")
}
wLogin := performJSON(r, http.MethodPost, "/login", map[string]any{
"email": "john@example.com",
"password": "secret123",
}, nil)
if wLogin.Code != http.StatusForbidden {
t.Fatalf("login expected 403 before verify, got %d body=%s", wLogin.Code, wLogin.Body.String())
}
verifyPath := "/verify-email?token=" + url.QueryEscape(verificationToken)
wVerify := performJSON(r, http.MethodGet, verifyPath, nil, nil)
if wVerify.Code != http.StatusOK {
t.Fatalf("verify expected 200, got %d body=%s", wVerify.Code, wVerify.Body.String())
}
var verifyResp map[string]any
if err := json.Unmarshal(wVerify.Body.Bytes(), &verifyResp); err != nil {
t.Fatalf("verify json parse failed: %v", err)
}
accessToken, _ = verifyResp["access"].(string)
refreshToken, _ := verifyResp["refresh"].(string)
if accessToken == "" || refreshToken == "" {
t.Fatalf("verify should return tokens")
}
assertJWTFormat(t, accessToken)
assertJWTFormat(t, refreshToken)
wLogin = performJSON(r, http.MethodPost, "/login", map[string]any{
"email": "john@example.com",
"password": "secret123",
}, nil)
if wLogin.Code != http.StatusOK {
t.Fatalf("login expected 200, got %d body=%s", wLogin.Code, wLogin.Body.String())
}
var loginResp map[string]any
if err := json.Unmarshal(wLogin.Body.Bytes(), &loginResp); err != nil {
t.Fatalf("login json parse failed: %v", err)
}
loginRefreshToken, _ := loginResp["refresh"].(string)
loginAccessToken, _ := loginResp["access"].(string)
assertJWTFormat(t, loginAccessToken)
assertJWTFormat(t, loginRefreshToken)
wMe := performJSON(r, http.MethodGet, "/me", nil, map[string]string{"Authorization": "Bearer " + accessToken})
if wMe.Code != http.StatusOK {
t.Fatalf("me expected 200, got %d", wMe.Code)
}
wRefresh := performJSON(r, http.MethodPost, "/refresh", map[string]any{"refresh_token": refreshToken}, nil)
if wRefresh.Code != http.StatusOK {
t.Fatalf("refresh expected 200, got %d body=%s", wRefresh.Code, wRefresh.Body.String())
}
var refreshResp map[string]any
if err := json.Unmarshal(wRefresh.Body.Bytes(), &refreshResp); err != nil {
t.Fatalf("refresh json parse failed: %v", err)
}
newRefreshToken, _ := refreshResp["refresh"].(string)
newAccessToken, _ := refreshResp["access"].(string)
assertJWTFormat(t, newAccessToken)
assertJWTFormat(t, newRefreshToken)
if newRefreshToken == refreshToken {
t.Fatalf("refresh rotation should return a new refresh token")
}
var oldToken models.RefreshToken
if err := configs.DB.Where("token_hash = ?", hashToken(refreshToken)).First(&oldToken).Error; err != nil {
t.Fatalf("refresh token record not found: %v", err)
}
if !oldToken.Revoked {
t.Fatalf("old refresh token should be revoked after refresh")
}
if oldToken.ReplacedByTokenID == "" {
t.Fatalf("old token should keep replaced_by_token_id")
}
}
func TestRegisterDuplicateEmail(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("JWT_SECRET", "test-secret")
setupHandlersTestDB(t)
r := gin.New()
r.POST("/register", Register)
payload := map[string]any{
"username": "user1",
"email": "dup@example.com",
"first_name": "User",
"last_name": "One",
"password": "secret123",
"confirm_password": "secret123",
}
w1 := performJSON(r, http.MethodPost, "/register", payload, nil)
if w1.Code != http.StatusCreated {
t.Fatalf("first register expected 201, got %d body=%s", w1.Code, w1.Body.String())
}
w2 := performJSON(r, http.MethodPost, "/register", payload, nil)
if w2.Code != http.StatusConflict {
t.Fatalf("second register expected 409, got %d", w2.Code)
}
}
func TestLoginWrongPassword(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("JWT_SECRET", "test-secret")
setupHandlersTestDB(t)
hash, err := bcrypt.GenerateFromPassword([]byte("correct-password"), bcrypt.DefaultCost)
if err != nil {
t.Fatalf("bcrypt failed: %v", err)
}
isAdmin := false
user := models.User{UserName: "u1", Email: "u1@example.com", Password: string(hash), IsAdmin: &isAdmin}
if err := configs.DB.Create(&user).Error; err != nil {
t.Fatalf("seed user failed: %v", err)
}
r := gin.New()
r.POST("/login", Login)
w := performJSON(r, http.MethodPost, "/login", map[string]any{
"email": "u1@example.com",
"password": "wrong-password",
}, nil)
if w.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", w.Code)
}
}
func TestMakeAdminWithAdminMiddleware(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("JWT_SECRET", "test-secret")
setupHandlersTestDB(t)
isAdmin := true
isUser := false
admin := models.User{UserName: "admin", Email: "admin@example.com", Password: "x", IsAdmin: &isAdmin}
target := models.User{UserName: "target", Email: "target@example.com", Password: "x", IsAdmin: &isUser}
if err := configs.DB.Create(&admin).Error; err != nil {
t.Fatalf("create admin failed: %v", err)
}
if err := configs.DB.Create(&target).Error; err != nil {
t.Fatalf("create target failed: %v", err)
}
token, err := middleware.BuildAccessTokenForUser(admin)
if err != nil {
t.Fatalf("token create failed: %v", err)
}
r := gin.New()
r.POST("/users/:id/admin", middleware.AuthRequired(), middleware.AdminRequired(), MakeAdmin)
w := performJSON(r, http.MethodPost, "/users/"+toString(target.ID)+"/admin", map[string]any{"is_admin": true}, map[string]string{
"Authorization": "Bearer " + token,
})
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String())
}
var updated models.User
if err := configs.DB.First(&updated, target.ID).Error; err != nil {
t.Fatalf("read updated user failed: %v", err)
}
if updated.IsAdmin == nil || !*updated.IsAdmin {
t.Fatalf("target user should be admin")
}
}
func TestRefreshRejectsExpiredToken(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("JWT_SECRET", "test-secret")
setupHandlersTestDB(t)
isAdmin := false
user := models.User{UserName: "u2", Email: "u2@example.com", Password: "x", IsAdmin: &isAdmin}
if err := configs.DB.Create(&user).Error; err != nil {
t.Fatalf("user create failed: %v", err)
}
refresh := "deadbeef"
record := models.RefreshToken{
UserID: uint64(user.ID),
TokenID: "tid1",
TokenHash: hashToken(refresh),
TokenFingerprint: tokenFingerprint(refresh),
ExpiresAt: time.Now().Add(-time.Minute),
}
if err := configs.DB.Create(&record).Error; err != nil {
t.Fatalf("refresh create failed: %v", err)
}
r := gin.New()
r.POST("/refresh", Refresh)
w := performJSON(r, http.MethodPost, "/refresh", map[string]any{"refresh_token": refresh}, nil)
if w.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", w.Code)
}
}
func TestIssueTokens_DefaultFlowKeepsSessionExpiryNilAndRefreshWorks(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("JWT_SECRET", "test-secret")
setupHandlersTestDB(t)
isAdmin := false
isActive := true
emailVerified := true
user := models.User{
UserName: "normal_user",
Email: "normal@example.com",
Password: "x",
IsAdmin: &isAdmin,
IsActive: &isActive,
EmailVerified: &emailVerified,
}
if err := configs.DB.Create(&user).Error; err != nil {
t.Fatalf("user create failed: %v", err)
}
_, refreshToken, _, err := issueTokens(user, "test-agent", "127.0.0.1")
if err != nil {
t.Fatalf("issueTokens failed: %v", err)
}
var record models.RefreshToken
if err := configs.DB.Where("token_hash = ?", hashToken(refreshToken)).First(&record).Error; err != nil {
t.Fatalf("refresh token record should exist: %v", err)
}
if record.SessionExpiresAt != nil {
t.Fatalf("default flow should keep session_expires_at nil")
}
r := gin.New()
r.POST("/refresh", Refresh)
w := performJSON(r, http.MethodPost, "/refresh", map[string]any{"refresh_token": refreshToken}, nil)
if w.Code != http.StatusOK {
t.Fatalf("refresh expected 200 for default flow, got %d body=%s", w.Code, w.Body.String())
}
}
func TestAdminScopedTokenIssuesOnlyAccessTokenWithoutRefreshRecord(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("JWT_SECRET", "test-secret")
setupHandlersTestDB(t)
isAdmin := true
isActive := true
emailVerified := true
adminUser := models.User{
UserName: "admin_user",
Email: "admin_scoped@example.com",
Password: "x",
IsAdmin: &isAdmin,
IsActive: &isActive,
EmailVerified: &emailVerified,
}
if err := configs.DB.Create(&adminUser).Error; err != nil {
t.Fatalf("admin user create failed: %v", err)
}
accessToken, err := middleware.BuildAccessTokenForUser(adminUser)
if err != nil {
t.Fatalf("build access token failed: %v", err)
}
r := gin.New()
r.POST("/admin/tokens/issue", middleware.AuthRequired(), middleware.AdminRequired(), IssueAdminScopedToken)
wIssue := performJSON(
r,
http.MethodPost,
"/admin/tokens/issue",
map[string]any{"duration_days": 45},
map[string]string{"Authorization": "Bearer " + accessToken},
)
if wIssue.Code != http.StatusOK {
t.Fatalf("issue endpoint expected 200, got %d body=%s", wIssue.Code, wIssue.Body.String())
}
var issueResp map[string]any
if err := json.Unmarshal(wIssue.Body.Bytes(), &issueResp); err != nil {
t.Fatalf("issue response parse failed: %v", err)
}
scopedAccess, _ := issueResp["access"].(string)
if scopedAccess == "" {
t.Fatalf("issued scoped access token should exist")
}
assertJWTFormat(t, scopedAccess)
if _, ok := issueResp["refresh"]; ok {
t.Fatalf("scoped token response should not include refresh token")
}
expiresAt, _ := issueResp["expires_at"].(string)
if expiresAt == "" {
t.Fatalf("scoped token response should include expires_at")
}
expiresAtTime, err := time.Parse(time.RFC3339, expiresAt)
if err != nil {
t.Fatalf("expires_at should be RFC3339: %v", err)
}
expected := time.Now().Add(45 * 24 * time.Hour)
if expiresAtTime.Before(expected.Add(-2*time.Minute)) || expiresAtTime.After(expected.Add(2*time.Minute)) {
t.Fatalf("expires_at should be close to requested duration, got=%s expected_around=%s", expiresAtTime, expected)
}
var refreshCount int64
if err := configs.DB.Model(&models.RefreshToken{}).Where("user_id = ?", adminUser.ID).Count(&refreshCount).Error; err != nil {
t.Fatalf("refresh token count query failed: %v", err)
}
if refreshCount != 0 {
t.Fatalf("scoped access token flow should not create refresh records, got %d", refreshCount)
}
}
func toString(v uint) string {
return strconv.FormatUint(uint64(v), 10)
}
func TestRegisterRejectsMismatchedConfirmPassword(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("JWT_SECRET", "test-secret")
setupHandlersTestDB(t)
r := gin.New()
r.POST("/register", Register)
w := performJSON(r, http.MethodPost, "/register", map[string]any{
"username": "user2",
"email": "user2@example.com",
"first_name": "User",
"last_name": "Two",
"password": "secret123",
"confirm_password": "wrong123",
}, nil)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d body=%s", w.Code, w.Body.String())
}
}
func TestGoogleSocialLoginCreatesVerifiedActiveUserAndProfile(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("JWT_SECRET", "test-secret")
setupHandlersTestDB(t)
provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/userinfo" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"sub":"g-123","email":"social@example.com","email_verified":true,"given_name":"Social","family_name":"User","picture":"https://cdn/avatar.png"}`))
}))
defer provider.Close()
prevGoogle := googleUserInfoURL
googleUserInfoURL = provider.URL + "/userinfo"
t.Cleanup(func() { googleUserInfoURL = prevGoogle })
r := gin.New()
r.POST("/auth/social/google", GoogleLogin)
w := performJSON(r, http.MethodPost, "/auth/social/google", map[string]any{"access_token": "token-abc"}, nil)
if w.Code != http.StatusOK {
dump, _ := httputil.DumpResponse(w.Result(), true)
t.Fatalf("expected 200, got %d body=%s", w.Code, string(dump))
}
var resp map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("response parse failed: %v", err)
}
if resp["provider"] != "google" {
t.Fatalf("provider should be google")
}
if access, _ := resp["access"].(string); access == "" {
t.Fatalf("access token should be returned")
}
var user models.User
if err := configs.DB.Where("email = ?", "social@example.com").First(&user).Error; err != nil {
t.Fatalf("user should be created: %v", err)
}
if user.EmailVerified == nil || !*user.EmailVerified {
t.Fatalf("social login user should be email verified")
}
if user.IsActive == nil || !*user.IsActive {
t.Fatalf("social login user should be active")
}
var profile models.Profile
if err := configs.DB.Where("user_id = ?", user.ID).First(&profile).Error; err != nil {
t.Fatalf("profile should be created: %v", err)
}
if profile.FirstName != "Social" || profile.LastName != "User" {
t.Fatalf("profile name mismatch: %+v", profile)
}
}
func TestGitHubSocialLoginReadsPrimaryEmail(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("JWT_SECRET", "test-secret")
setupHandlersTestDB(t)
provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/user":
_, _ = w.Write([]byte(`{"id":99,"login":"octo","name":"Octo Cat","email":"","avatar_url":"https://cdn/octo.png"}`))
case "/user/emails":
_, _ = w.Write([]byte(`[{"email":"octo@example.com","primary":true,"verified":true}]`))
default:
http.NotFound(w, r)
}
}))
defer provider.Close()
prevUser := githubUserURL
prevEmails := githubEmailsURL
githubUserURL = provider.URL + "/user"
githubEmailsURL = provider.URL + "/user/emails"
t.Cleanup(func() {
githubUserURL = prevUser
githubEmailsURL = prevEmails
})
r := gin.New()
r.POST("/auth/social/github", GitHubLogin)
w := performJSON(r, http.MethodPost, "/auth/social/github", map[string]any{"access_token": "gh-token"}, nil)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String())
}
var social models.SocialAccount
if err := configs.DB.Where("provider = ? AND provider_id = ?", "github", "99").First(&social).Error; err != nil {
t.Fatalf("github social account should be created: %v", err)
}
if social.Email != "octo@example.com" {
t.Fatalf("github email should come from /user/emails, got %s", social.Email)
}
}
func TestMeProfileGetAndUpdate(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("JWT_SECRET", "test-secret")
setupHandlersTestDB(t)
isActive := true
emailVerified := true
isAdmin := false
user := models.User{
UserName: "profile_user",
Email: "profile@example.com",
Password: "x",
IsActive: &isActive,
EmailVerified: &emailVerified,
IsAdmin: &isAdmin,
}
if err := configs.DB.Create(&user).Error; err != nil {
t.Fatalf("create user failed: %v", err)
}
oldAvatarURL, oldAvatarPath := createOldAvatarFixture(t, "old_user_avatar.png")
profile := models.Profile{UserID: uint64(user.ID), FirstName: "Test", LastName: "User", AvatarURL: oldAvatarURL}
if err := configs.DB.Create(&profile).Error; err != nil {
t.Fatalf("create profile failed: %v", err)
}
token, err := middleware.BuildAccessTokenForUser(user)
if err != nil {
t.Fatalf("token create failed: %v", err)
}
r := gin.New()
r.GET("/me/profile", middleware.AuthRequired(), GetMyProfile)
r.PUT("/me/profile", middleware.AuthRequired(), UpdateMyProfile)
wGet := performJSON(r, http.MethodGet, "/me/profile", nil, map[string]string{"Authorization": "Bearer " + token})
if wGet.Code != http.StatusOK {
t.Fatalf("get profile expected 200, got %d body=%s", wGet.Code, wGet.Body.String())
}
wPut := performMultipart(
r,
http.MethodPut,
"/me/profile",
map[string]string{"first_name": "Yeni", "last_name": "Isim"},
"avatar",
"avatar.png",
tinyPNGFixture(t),
map[string]string{"Authorization": "Bearer " + token},
)
if wPut.Code != http.StatusOK {
t.Fatalf("update profile expected 200, got %d body=%s", wPut.Code, wPut.Body.String())
}
var updated models.Profile
if err := configs.DB.Where("user_id = ?", user.ID).First(&updated).Error; err != nil {
t.Fatalf("read updated profile failed: %v", err)
}
if updated.FirstName != "Yeni" || updated.LastName != "Isim" {
t.Fatalf("profile name not updated: %+v", updated)
}
if !strings.HasPrefix(updated.AvatarURL, "/uploads/avatars/") {
t.Fatalf("avatar path not updated: %s", updated.AvatarURL)
}
if _, err := os.Stat(oldAvatarPath); !os.IsNotExist(err) {
t.Fatalf("old avatar should be deleted, err=%v", err)
}
}
func TestMeProfileUpdateCreatesProfileWhenMissing(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("JWT_SECRET", "test-secret")
setupHandlersTestDB(t)
isActive := true
emailVerified := true
isAdmin := false
user := models.User{
UserName: "legacy_user",
Email: "legacy@example.com",
Password: "x",
IsActive: &isActive,
EmailVerified: &emailVerified,
IsAdmin: &isAdmin,
}
if err := configs.DB.Create(&user).Error; err != nil {
t.Fatalf("create user failed: %v", err)
}
token, err := middleware.BuildAccessTokenForUser(user)
if err != nil {
t.Fatalf("token create failed: %v", err)
}
r := gin.New()
r.PUT("/me/profile", middleware.AuthRequired(), UpdateMyProfile)
wPut := performMultipart(
r,
http.MethodPut,
"/me/profile",
map[string]string{"first_name": "Beyhan", "last_name": "Ogur"},
"avatar",
"avatar.png",
tinyPNGFixture(t),
map[string]string{"Authorization": "Bearer " + token},
)
if wPut.Code != http.StatusOK {
t.Fatalf("update profile expected 200, got %d body=%s", wPut.Code, wPut.Body.String())
}
var profile models.Profile
if err := configs.DB.Where("user_id = ?", user.ID).First(&profile).Error; err != nil {
t.Fatalf("profile should be auto-created: %v", err)
}
if profile.FirstName != "Beyhan" || profile.LastName != "Ogur" {
t.Fatalf("profile fields mismatch: %+v", profile)
}
}