diff --git a/backend/internal/routers/user_router_test.go b/backend/internal/routers/user_router_test.go index ca65769..12079b9 100644 --- a/backend/internal/routers/user_router_test.go +++ b/backend/internal/routers/user_router_test.go @@ -2,7 +2,6 @@ package routers_test import ( "encoding/json" - "fmt" "io" "net/http" "net/http/httptest" @@ -30,10 +29,7 @@ func testRouterFactory(t *testing.T, testCfg *config.Config, setDBDown bool) *gi testLogger := testutil.NewTestLogger() - if testCfg.DbAddress == "file::memory:?cache=shared" { - safeName := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name()) - testCfg.DbAddress = fmt.Sprintf("file:%s?mode=memory&cache=shared", safeName) - } + testCfg.DbAddress = testutil.GetSafeTestDBName(testCfg.DbAddress, t.Name()) myDB, err := db.GetDB(testCfg.DbAddress, testLogger) if err != nil { diff --git a/backend/internal/service/friend_service_test.go b/backend/internal/service/friend_service_test.go new file mode 100644 index 0000000..9663b92 --- /dev/null +++ b/backend/internal/service/friend_service_test.go @@ -0,0 +1,230 @@ +package service_test + +import ( + "context" + "errors" + "testing" + "time" + + authError "github.com/paularynty/transcendence/auth-service-go/internal/auth_error" + "github.com/paularynty/transcendence/auth-service-go/internal/db" + "github.com/paularynty/transcendence/auth-service-go/internal/dto" + "github.com/paularynty/transcendence/auth-service-go/internal/testutil" + "gorm.io/gorm" +) + +func createFriend(t *testing.T, myDB *gorm.DB, userID, friendID uint) { + t.Helper() + + friend := db.Friend{ + UserID: userID, + FriendID: friendID, + } + if err := gorm.G[db.Friend](myDB).Create(context.Background(), &friend); err != nil { + t.Fatalf("failed to create friend, err: %v", err) + } +} + +func createHeartbeat(t *testing.T, myDB *gorm.DB, userID uint, lastSeen time.Time) { + t.Helper() + + hb := db.HeartBeat{ + UserID: userID, + LastSeenAt: lastSeen, + } + if err := gorm.G[db.HeartBeat](myDB).Create(context.Background(), &hb); err != nil { + t.Fatalf("failed to create heartbeat, err: %v", err) + } +} + +func TestGetAllUsersLimitedInfo(t *testing.T) { + t.Run("empty list", func(t *testing.T) { + userService, _ := testutil.NewTestUserService(t) + + got, err := userService.GetAllUsersLimitedInfo(context.Background()) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + if len(got) != 0 { + t.Fatalf("expected 0 users, got %d", len(got)) + } + }) + + t.Run("returns simple users", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + avatar1 := "https://example.com/1.png" + avatar2 := "https://example.com/2.png" + u1 := testutil.CreateUser(t, myDB, "alice", "alice@example.com", &avatar1) + u2 := testutil.CreateUser(t, myDB, "bob", "bob@example.com", &avatar2) + + expected := map[uint]dto.SimpleUser{ + u1.ID: {ID: u1.ID, Username: "alice", Avatar: &avatar1}, + u2.ID: {ID: u2.ID, Username: "bob", Avatar: &avatar2}, + } + + got, err := userService.GetAllUsersLimitedInfo(context.Background()) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + if len(got) != 2 { + t.Fatalf("expected 2 users, got %d", len(got)) + } + + gotMap := make(map[uint]dto.SimpleUser, len(got)) + for _, u := range got { + gotMap[u.ID] = u + } + + for id, exp := range expected { + gotUser, ok := gotMap[id] + if !ok { + t.Fatalf("missing user id %d", id) + } + if gotUser.Username != exp.Username { + t.Fatalf("user %d username mismatch: expected %s, got %s", id, exp.Username, gotUser.Username) + } + if exp.Avatar == nil && gotUser.Avatar != nil { + t.Fatalf("user %d expected nil avatar, got %v", id, gotUser.Avatar) + } + if exp.Avatar != nil && (gotUser.Avatar == nil || *gotUser.Avatar != *exp.Avatar) { + t.Fatalf("user %d avatar mismatch: expected %v, got %v", id, exp.Avatar, gotUser.Avatar) + } + } + }) +} + +func TestGetUserFriends(t *testing.T) { + t.Run("no friends", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + u := testutil.CreateUser(t, myDB, "solo", "solo@example.com", nil) + + got, err := userService.GetUserFriends(context.Background(), u.ID) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + if len(got) != 0 { + t.Fatalf("expected 0 friends, got %d", len(got)) + } + }) + + t.Run("friends with online status", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user := testutil.CreateUser(t, myDB, "owner", "owner@example.com", nil) + friend1 := testutil.CreateUser(t, myDB, "friend1", "friend1@example.com", nil) + friend2 := testutil.CreateUser(t, myDB, "friend2", "friend2@example.com", nil) + + createFriend(t, myDB, user.ID, friend1.ID) + createFriend(t, myDB, user.ID, friend2.ID) + + createHeartbeat(t, myDB, friend2.ID, time.Now()) + + expectedOnline := map[uint]bool{ + friend1.ID: false, + friend2.ID: true, + } + + got, err := userService.GetUserFriends(context.Background(), user.ID) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + if len(got) != 2 { + t.Fatalf("expected 2 friends, got %d", len(got)) + } + + gotOnline := make(map[uint]bool, len(got)) + for _, f := range got { + gotOnline[f.ID] = f.Online + } + + for friendID, expected := range expectedOnline { + isOnline, ok := gotOnline[friendID] + if !ok { + t.Fatalf("missing friend id %d", friendID) + } + if isOnline != expected { + t.Fatalf("friend id %d online mismatch: expected %v, got %v", friendID, expected, isOnline) + } + } + }) +} + +func TestAddNewFriend(t *testing.T) { + t.Run("cannot add yourself", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user := testutil.CreateUser(t, myDB, "self", "self@example.com", nil) + + err := userService.AddNewFriend(context.Background(), user.ID, &dto.AddNewFriendRequest{UserID: user.ID}) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 400 { + t.Fatalf("expected status 400, got %d", authErr.Status) + } + }) + + t.Run("user not found", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user := testutil.CreateUser(t, myDB, "owner", "owner@example.com", nil) + + err := userService.AddNewFriend(context.Background(), user.ID, &dto.AddNewFriendRequest{UserID: user.ID + 999}) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 404 { + t.Fatalf("expected status 404, got %d", authErr.Status) + } + }) + + t.Run("friend already added", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user := testutil.CreateUser(t, myDB, "owner", "owner@example.com", nil) + friend := testutil.CreateUser(t, myDB, "friend", "friend@example.com", nil) + createFriend(t, myDB, user.ID, friend.ID) + + err := userService.AddNewFriend(context.Background(), user.ID, &dto.AddNewFriendRequest{UserID: friend.ID}) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 409 { + t.Fatalf("expected status 409, got %d", authErr.Status) + } + }) + + t.Run("success", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user := testutil.CreateUser(t, myDB, "owner", "owner@example.com", nil) + friend := testutil.CreateUser(t, myDB, "friend", "friend@example.com", nil) + + err := userService.AddNewFriend(context.Background(), user.ID, &dto.AddNewFriendRequest{UserID: friend.ID}) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + + friendRecord, err := gorm.G[db.Friend](myDB).Where("user_id = ? AND friend_id = ?", user.ID, friend.ID).First(context.Background()) + if err != nil { + t.Fatalf("failed to query friend record, err: %v", err) + } + if friendRecord.UserID != user.ID || friendRecord.FriendID != friend.ID { + t.Fatalf("unexpected friend record values") + } + }) +} diff --git a/backend/internal/service/google_oauth_service_test.go b/backend/internal/service/google_oauth_service_test.go new file mode 100644 index 0000000..7a1c837 --- /dev/null +++ b/backend/internal/service/google_oauth_service_test.go @@ -0,0 +1,215 @@ +package service_test + +import ( + "context" + "errors" + "net/url" + "testing" + + "cloud.google.com/go/auth/credentials/idtoken" + authError "github.com/paularynty/transcendence/auth-service-go/internal/auth_error" + "github.com/paularynty/transcendence/auth-service-go/internal/db" + "github.com/paularynty/transcendence/auth-service-go/internal/dependency" + "github.com/paularynty/transcendence/auth-service-go/internal/dto" + "github.com/paularynty/transcendence/auth-service-go/internal/service" + "github.com/paularynty/transcendence/auth-service-go/internal/testutil" + "github.com/paularynty/transcendence/auth-service-go/internal/util/jwt" + "gorm.io/gorm" +) + +func TestHandleGoogleOAuthCallback(t *testing.T) { + t.Run("invalid state token", func(t *testing.T) { + userService, _ := testutil.NewTestUserService(t) + + redirect := userService.HandleGoogleOAuthCallback(context.Background(), "code", "bad-state") + u, err := url.Parse(redirect) + if err != nil { + t.Fatalf("failed to parse redirect url, err: %v", err) + } + if u.Query().Get("error") == "" { + t.Fatalf("expected error query param") + } + if u.Query().Get("token") != "" { + t.Fatalf("did not expect token query param") + } + }) + + t.Run("exchange code failure", func(t *testing.T) { + userService, _ := testutil.NewTestUserService(t) + + origExchange := service.ExchangeCodeForTokens + t.Cleanup(func() { service.ExchangeCodeForTokens = origExchange }) + service.ExchangeCodeForTokens = func(dep *dependency.Dependency, ctx context.Context, code string) (*idtoken.Payload, error) { + return nil, errors.New("exchange failed") + } + + state, err := jwt.SignOauthStateToken(userService.Dep) + if err != nil { + t.Fatalf("failed to sign state token, err: %v", err) + } + + redirect := userService.HandleGoogleOAuthCallback(context.Background(), "code", state) + u, err := url.Parse(redirect) + if err != nil { + t.Fatalf("failed to parse redirect url, err: %v", err) + } + if u.Query().Get("error") == "" { + t.Fatalf("expected error query param") + } + }) + + t.Run("fetch google user info failure", func(t *testing.T) { + userService, _ := testutil.NewTestUserService(t) + + origExchange := service.ExchangeCodeForTokens + origFetch := service.FetchGoogleUserInfo + t.Cleanup(func() { + service.ExchangeCodeForTokens = origExchange + service.FetchGoogleUserInfo = origFetch + }) + + service.ExchangeCodeForTokens = func(dep *dependency.Dependency, ctx context.Context, code string) (*idtoken.Payload, error) { + return &idtoken.Payload{Subject: "gid", Claims: map[string]any{}}, nil + } + service.FetchGoogleUserInfo = func(payload *idtoken.Payload) (*dto.GoogleUserData, error) { + return nil, authError.NewAuthError(400, "bad token") + } + + state, err := jwt.SignOauthStateToken(userService.Dep) + if err != nil { + t.Fatalf("failed to sign state token, err: %v", err) + } + + redirect := userService.HandleGoogleOAuthCallback(context.Background(), "code", state) + u, err := url.Parse(redirect) + if err != nil { + t.Fatalf("failed to parse redirect url, err: %v", err) + } + if u.Query().Get("error") == "" { + t.Fatalf("expected error query param") + } + }) + + t.Run("existing google oauth id logs in", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + origExchange := service.ExchangeCodeForTokens + t.Cleanup(func() { service.ExchangeCodeForTokens = origExchange }) + service.ExchangeCodeForTokens = func(dep *dependency.Dependency, ctx context.Context, code string) (*idtoken.Payload, error) { + return &idtoken.Payload{ + Subject: "gid-1", + Claims: map[string]any{ + "email": "gid@example.com", + "name": "Gid", + }, + }, nil + } + + googleID := "gid-1" + user := db.User{ + Username: "giduser", + Email: "gid@example.com", + GoogleOauthID: &googleID, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + state, err := jwt.SignOauthStateToken(userService.Dep) + if err != nil { + t.Fatalf("failed to sign state token, err: %v", err) + } + + redirect := userService.HandleGoogleOAuthCallback(context.Background(), "code", state) + u, err := url.Parse(redirect) + if err != nil { + t.Fatalf("failed to parse redirect url, err: %v", err) + } + if u.Query().Get("token") == "" { + t.Fatalf("expected token query param") + } + if u.Query().Get("error") != "" { + t.Fatalf("did not expect error query param") + } + }) + + t.Run("existing email fails to link google account", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + origExchange := service.ExchangeCodeForTokens + t.Cleanup(func() { service.ExchangeCodeForTokens = origExchange }) + service.ExchangeCodeForTokens = func(dep *dependency.Dependency, ctx context.Context, code string) (*idtoken.Payload, error) { + return &idtoken.Payload{ + Subject: "gid-2", + Claims: map[string]any{ + "email": "exists@example.com", + "name": "Exists", + }, + }, nil + } + + user := db.User{ + Username: "exists", + Email: "exists@example.com", + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + state, err := jwt.SignOauthStateToken(userService.Dep) + if err != nil { + t.Fatalf("failed to sign state token, err: %v", err) + } + + redirect := userService.HandleGoogleOAuthCallback(context.Background(), "code", state) + u, err := url.Parse(redirect) + if err != nil { + t.Fatalf("failed to parse redirect url, err: %v", err) + } + if u.Query().Get("error") == "" { + t.Fatalf("expected error query param") + } + }) + + t.Run("new user created", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + origExchange := service.ExchangeCodeForTokens + t.Cleanup(func() { service.ExchangeCodeForTokens = origExchange }) + service.ExchangeCodeForTokens = func(dep *dependency.Dependency, ctx context.Context, code string) (*idtoken.Payload, error) { + return &idtoken.Payload{ + Subject: "gid-3", + Claims: map[string]any{ + "email": "new@example.com", + "name": "New", + "picture": "https://example.com/a.png", + }, + }, nil + } + + state, err := jwt.SignOauthStateToken(userService.Dep) + if err != nil { + t.Fatalf("failed to sign state token, err: %v", err) + } + + redirect := userService.HandleGoogleOAuthCallback(context.Background(), "code", state) + u, err := url.Parse(redirect) + if err != nil { + t.Fatalf("failed to parse redirect url, err: %v", err) + } + if u.Query().Get("token") == "" { + t.Fatalf("expected token query param") + } + if u.Query().Get("error") != "" { + t.Fatalf("did not expect error query param") + } + + modelUser, err := gorm.G[db.User](myDB).Where("email = ?", "new@example.com").First(context.Background()) + if err != nil { + t.Fatalf("expected user to be created, err: %v", err) + } + if modelUser.GoogleOauthID == nil || *modelUser.GoogleOauthID != "gid-3" { + t.Fatalf("expected google oauth id to be set") + } + }) +} diff --git a/backend/internal/service/twofa_service_test.go b/backend/internal/service/twofa_service_test.go new file mode 100644 index 0000000..82020a8 --- /dev/null +++ b/backend/internal/service/twofa_service_test.go @@ -0,0 +1,538 @@ +package service_test + +import ( + "context" + "errors" + "testing" + "time" + + authError "github.com/paularynty/transcendence/auth-service-go/internal/auth_error" + "github.com/paularynty/transcendence/auth-service-go/internal/db" + "github.com/paularynty/transcendence/auth-service-go/internal/dto" + "github.com/paularynty/transcendence/auth-service-go/internal/testutil" + "github.com/paularynty/transcendence/auth-service-go/internal/util/jwt" + "github.com/pquerna/otp/totp" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +func TestStartTwoFaSetup(t *testing.T) { + t.Run("user not found", func(t *testing.T) { + userService, _ := testutil.NewTestUserService(t) + + _, err := userService.StartTwoFaSetup(context.Background(), 9999) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 404 { + t.Fatalf("expected status 404, got %d", authErr.Status) + } + }) + + t.Run("already enabled", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + secret := "enabled-secret" + user := db.User{ + Username: "user1", + Email: "user1@example.com", + TwoFAToken: &secret, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + _, err := userService.StartTwoFaSetup(context.Background(), user.ID) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 400 { + t.Fatalf("expected status 400, got %d", authErr.Status) + } + }) + + t.Run("google oauth user", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + googleID := "gid-1" + user := db.User{ + Username: "user2", + Email: "user2@example.com", + GoogleOauthID: &googleID, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + _, err := userService.StartTwoFaSetup(context.Background(), user.ID) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 400 { + t.Fatalf("expected status 400, got %d", authErr.Status) + } + }) + + t.Run("success", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user := db.User{ + Username: "user3", + Email: "user3@example.com", + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + resp, err := userService.StartTwoFaSetup(context.Background(), user.ID) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + if resp.TwoFASecret == "" || resp.SetupToken == "" || resp.TwoFaUri == "" { + t.Fatalf("expected non-empty 2FA setup response") + } + + modelUser, err := gorm.G[db.User](myDB).Where("id = ?", user.ID).First(context.Background()) + if err != nil { + t.Fatalf("failed to query user, err: %v", err) + } + if modelUser.TwoFAToken == nil || *modelUser.TwoFAToken != "pre-"+resp.TwoFASecret { + t.Fatalf("expected two_fa_token to be prefixed") + } + }) +} + +func TestConfirmTwoFaSetup(t *testing.T) { + t.Run("invalid setup token", func(t *testing.T) { + userService, _ := testutil.NewTestUserService(t) + + _, err := userService.ConfirmTwoFaSetup(context.Background(), 1, &dto.TwoFAConfirmRequest{ + TwoFACode: "123456", + SetupToken: "bad-token", + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 400 { + t.Fatalf("expected status 400, got %d", authErr.Status) + } + }) + + t.Run("setup token user mismatch", func(t *testing.T) { + userService, _ := testutil.NewTestUserService(t) + + token, err := jwt.SignTwoFASetupToken(userService.Dep, 123, "secret") + if err != nil { + t.Fatalf("failed to sign setup token, err: %v", err) + } + + _, err = userService.ConfirmTwoFaSetup(context.Background(), 999, &dto.TwoFAConfirmRequest{ + TwoFACode: "123456", + SetupToken: token, + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 400 { + t.Fatalf("expected status 400, got %d", authErr.Status) + } + }) + + t.Run("setup not initiated", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user := db.User{ + Username: "user1", + Email: "user1@example.com", + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + token, err := jwt.SignTwoFASetupToken(userService.Dep, user.ID, "secret") + if err != nil { + t.Fatalf("failed to sign setup token, err: %v", err) + } + + _, err = userService.ConfirmTwoFaSetup(context.Background(), user.ID, &dto.TwoFAConfirmRequest{ + TwoFACode: "123456", + SetupToken: token, + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 400 { + t.Fatalf("expected status 400, got %d", authErr.Status) + } + }) + + t.Run("invalid 2FA code", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + secret := "JBSWY3DPEHPK3PXP" + preSecret := "pre-" + secret + user := db.User{ + Username: "user2", + Email: "user2@example.com", + TwoFAToken: &preSecret, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + token, err := jwt.SignTwoFASetupToken(userService.Dep, user.ID, secret) + if err != nil { + t.Fatalf("failed to sign setup token, err: %v", err) + } + + _, err = userService.ConfirmTwoFaSetup(context.Background(), user.ID, &dto.TwoFAConfirmRequest{ + TwoFACode: "000000", + SetupToken: token, + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 400 { + t.Fatalf("expected status 400, got %d", authErr.Status) + } + }) + + t.Run("success", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + secret, err := totp.Generate(totp.GenerateOpts{ + Issuer: "Transcendence", + AccountName: "user3@example.com", + }) + if err != nil { + t.Fatalf("failed to generate secret, err: %v", err) + } + preSecret := "pre-" + secret.Secret() + user := db.User{ + Username: "user3", + Email: "user3@example.com", + TwoFAToken: &preSecret, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + setupToken, err := jwt.SignTwoFASetupToken(userService.Dep, user.ID, secret.Secret()) + if err != nil { + t.Fatalf("failed to sign setup token, err: %v", err) + } + code, err := totp.GenerateCode(secret.Secret(), time.Now()) + if err != nil { + t.Fatalf("failed to generate code, err: %v", err) + } + + resp, err := userService.ConfirmTwoFaSetup(context.Background(), user.ID, &dto.TwoFAConfirmRequest{ + TwoFACode: code, + SetupToken: setupToken, + }) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + if resp.Token == "" { + t.Fatalf("expected token in response") + } + + modelUser, err := gorm.G[db.User](myDB).Where("id = ?", user.ID).First(context.Background()) + if err != nil { + t.Fatalf("failed to query user, err: %v", err) + } + if modelUser.TwoFAToken == nil || *modelUser.TwoFAToken != secret.Secret() { + t.Fatalf("expected two_fa_token to be enabled") + } + }) +} + +func TestDisableTwoFA(t *testing.T) { + t.Run("oauth user", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + secret := "enabled-secret" + user := db.User{ + Username: "user1", + Email: "user1@example.com", + TwoFAToken: &secret, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + _, err := userService.DisableTwoFA(context.Background(), user.ID, &dto.DisableTwoFARequest{ + Password: dto.Password{Password: "password"}, + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 400 { + t.Fatalf("expected status 400, got %d", authErr.Status) + } + }) + + t.Run("2FA not enabled", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + hash, err := bcrypt.GenerateFromPassword([]byte("password"), 10) + if err != nil { + t.Fatalf("failed to hash password, err: %v", err) + } + passwordHash := string(hash) + user := db.User{ + Username: "user2", + Email: "user2@example.com", + PasswordHash: &passwordHash, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + _, err = userService.DisableTwoFA(context.Background(), user.ID, &dto.DisableTwoFARequest{ + Password: dto.Password{Password: "password"}, + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 400 { + t.Fatalf("expected status 400, got %d", authErr.Status) + } + }) + + t.Run("invalid password", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + hash, err := bcrypt.GenerateFromPassword([]byte("password"), 10) + if err != nil { + t.Fatalf("failed to hash password, err: %v", err) + } + passwordHash := string(hash) + secret := "enabled-secret" + user := db.User{ + Username: "user3", + Email: "user3@example.com", + PasswordHash: &passwordHash, + TwoFAToken: &secret, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + _, err = userService.DisableTwoFA(context.Background(), user.ID, &dto.DisableTwoFARequest{ + Password: dto.Password{Password: "wrong"}, + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 401 { + t.Fatalf("expected status 401, got %d", authErr.Status) + } + }) + + t.Run("success", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + hash, err := bcrypt.GenerateFromPassword([]byte("password"), 10) + if err != nil { + t.Fatalf("failed to hash password, err: %v", err) + } + passwordHash := string(hash) + secret := "enabled-secret" + user := db.User{ + Username: "user4", + Email: "user4@example.com", + PasswordHash: &passwordHash, + TwoFAToken: &secret, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + resp, err := userService.DisableTwoFA(context.Background(), user.ID, &dto.DisableTwoFARequest{ + Password: dto.Password{Password: "password"}, + }) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + if resp.Token == "" { + t.Fatalf("expected token in response") + } + + modelUser, err := gorm.G[db.User](myDB).Where("id = ?", user.ID).First(context.Background()) + if err != nil { + t.Fatalf("failed to query user, err: %v", err) + } + if modelUser.TwoFAToken != nil { + t.Fatalf("expected two_fa_token to be cleared") + } + }) +} + +func TestSubmitTwoFAChallenge(t *testing.T) { + t.Run("invalid session token", func(t *testing.T) { + userService, _ := testutil.NewTestUserService(t) + + _, err := userService.SubmitTwoFAChallenge(context.Background(), &dto.TwoFAChallengeRequest{ + TwoFACode: "123456", + SessionToken: "bad-token", + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 400 { + t.Fatalf("expected status 400, got %d", authErr.Status) + } + }) + + t.Run("2FA not enabled", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user := db.User{ + Username: "user1", + Email: "user1@example.com", + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + token, err := jwt.SignTwoFAToken(userService.Dep, user.ID) + if err != nil { + t.Fatalf("failed to sign session token, err: %v", err) + } + + _, err = userService.SubmitTwoFAChallenge(context.Background(), &dto.TwoFAChallengeRequest{ + TwoFACode: "123456", + SessionToken: token, + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 400 { + t.Fatalf("expected status 400, got %d", authErr.Status) + } + }) + + t.Run("invalid 2FA code", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + secret := "JBSWY3DPEHPK3PXP" + user := db.User{ + Username: "user2", + Email: "user2@example.com", + TwoFAToken: &secret, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + token, err := jwt.SignTwoFAToken(userService.Dep, user.ID) + if err != nil { + t.Fatalf("failed to sign session token, err: %v", err) + } + + _, err = userService.SubmitTwoFAChallenge(context.Background(), &dto.TwoFAChallengeRequest{ + TwoFACode: "000000", + SessionToken: token, + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 400 { + t.Fatalf("expected status 400, got %d", authErr.Status) + } + }) + + t.Run("success", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + secret, err := totp.Generate(totp.GenerateOpts{ + Issuer: "Transcendence", + AccountName: "user3@example.com", + }) + if err != nil { + t.Fatalf("failed to generate secret, err: %v", err) + } + secretStr := secret.Secret() + user := db.User{ + Username: "user3", + Email: "user3@example.com", + TwoFAToken: &secretStr, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + token, err := jwt.SignTwoFAToken(userService.Dep, user.ID) + if err != nil { + t.Fatalf("failed to sign session token, err: %v", err) + } + code, err := totp.GenerateCode(secret.Secret(), time.Now()) + if err != nil { + t.Fatalf("failed to generate code, err: %v", err) + } + + resp, err := userService.SubmitTwoFAChallenge(context.Background(), &dto.TwoFAChallengeRequest{ + TwoFACode: code, + SessionToken: token, + }) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + if resp.Token == "" { + t.Fatalf("expected token in response") + } + }) +} diff --git a/backend/internal/service/user_service_test.go b/backend/internal/service/user_service_test.go new file mode 100644 index 0000000..0041b19 --- /dev/null +++ b/backend/internal/service/user_service_test.go @@ -0,0 +1,715 @@ +package service_test + +import ( + "context" + "errors" + "testing" + + authError "github.com/paularynty/transcendence/auth-service-go/internal/auth_error" + "github.com/paularynty/transcendence/auth-service-go/internal/db" + "github.com/paularynty/transcendence/auth-service-go/internal/dto" + "github.com/paularynty/transcendence/auth-service-go/internal/testutil" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +func TestGetDependency(t *testing.T) { + userService, _ := testutil.NewTestUserService(t) + if userService.GetDependency() != userService.Dep { + t.Fatalf("expected dependency to match") + } +} + +func TestCreateUser(t *testing.T) { + t.Run("success", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + req := &dto.CreateUserRequest{ + User: dto.User{ + UserName: dto.UserName{Username: "alice"}, + Email: "alice@example.com", + }, + Password: dto.Password{Password: "Password.777"}, + } + + resp, err := userService.CreateUser(context.Background(), req) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + if resp.Username != "alice" { + t.Fatalf("unexpected username: %s", resp.Username) + } + + modelUser, err := gorm.G[db.User](myDB).Where("username = ?", "alice").First(context.Background()) + if err != nil { + t.Fatalf("failed to query user, err: %v", err) + } + if modelUser.PasswordHash == nil || *modelUser.PasswordHash == "" { + t.Fatalf("expected password hash to be set") + } + }) + + t.Run("duplicate username", func(t *testing.T) { + userService, _ := testutil.NewTestUserService(t) + + req1 := &dto.CreateUserRequest{ + User: dto.User{ + UserName: dto.UserName{Username: "dupuser"}, + Email: "dup1@example.com", + }, + Password: dto.Password{Password: "Password.777"}, + } + _, err := userService.CreateUser(context.Background(), req1) + if err != nil { + t.Fatalf("unexpected error creating user, err: %v", err) + } + + req2 := &dto.CreateUserRequest{ + User: dto.User{ + UserName: dto.UserName{Username: "dupuser"}, + Email: "dup2@example.com", + }, + Password: dto.Password{Password: "Password.777"}, + } + _, err = userService.CreateUser(context.Background(), req2) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 409 { + t.Fatalf("expected status 409, got %d", authErr.Status) + } + }) + + t.Run("duplicate email", func(t *testing.T) { + userService, _ := testutil.NewTestUserService(t) + + req1 := &dto.CreateUserRequest{ + User: dto.User{ + UserName: dto.UserName{Username: "dupemail1"}, + Email: "dup@example.com", + }, + Password: dto.Password{Password: "Password.777"}, + } + _, err := userService.CreateUser(context.Background(), req1) + if err != nil { + t.Fatalf("unexpected error creating user, err: %v", err) + } + + req2 := &dto.CreateUserRequest{ + User: dto.User{ + UserName: dto.UserName{Username: "dupemail2"}, + Email: "dup@example.com", + }, + Password: dto.Password{Password: "Password.777"}, + } + _, err = userService.CreateUser(context.Background(), req2) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 409 { + t.Fatalf("expected status 409, got %d", authErr.Status) + } + }) +} + +func TestLoginUser(t *testing.T) { + t.Run("user not found", func(t *testing.T) { + userService, _ := testutil.NewTestUserService(t) + + _, err := userService.LoginUser(context.Background(), &dto.LoginUserRequest{ + Identifier: dto.Identifier{Identifier: "missing@example.com"}, + Password: dto.Password{Password: "Password.777"}, + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 401 { + t.Fatalf("expected status 401, got %d", authErr.Status) + } + }) + + t.Run("invalid credentials", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + hash, err := bcrypt.GenerateFromPassword([]byte("Password.777"), 10) + if err != nil { + t.Fatalf("failed to hash password, err: %v", err) + } + passwordHash := string(hash) + user := db.User{ + Username: "alice", + Email: "alice@example.com", + PasswordHash: &passwordHash, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + _, err = userService.LoginUser(context.Background(), &dto.LoginUserRequest{ + Identifier: dto.Identifier{Identifier: "alice"}, + Password: dto.Password{Password: "Wrong.777"}, + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 401 { + t.Fatalf("expected status 401, got %d", authErr.Status) + } + }) + + t.Run("login by username", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + hash, err := bcrypt.GenerateFromPassword([]byte("Password.777"), 10) + if err != nil { + t.Fatalf("failed to hash password, err: %v", err) + } + passwordHash := string(hash) + user := db.User{ + Username: "alice", + Email: "alice@example.com", + PasswordHash: &passwordHash, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + result, err := userService.LoginUser(context.Background(), &dto.LoginUserRequest{ + Identifier: dto.Identifier{Identifier: "alice"}, + Password: dto.Password{Password: "Password.777"}, + }) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + if result.User == nil || result.User.Token == "" { + t.Fatalf("expected user token") + } + }) + + t.Run("login by email", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + hash, err := bcrypt.GenerateFromPassword([]byte("Password.777"), 10) + if err != nil { + t.Fatalf("failed to hash password, err: %v", err) + } + passwordHash := string(hash) + user := db.User{ + Username: "alice", + Email: "alice@example.com", + PasswordHash: &passwordHash, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + result, err := userService.LoginUser(context.Background(), &dto.LoginUserRequest{ + Identifier: dto.Identifier{Identifier: "alice@example.com"}, + Password: dto.Password{Password: "Password.777"}, + }) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + if result.User == nil || result.User.Token == "" { + t.Fatalf("expected user token") + } + }) + + t.Run("2FA pending", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + hash, err := bcrypt.GenerateFromPassword([]byte("Password.777"), 10) + if err != nil { + t.Fatalf("failed to hash password, err: %v", err) + } + passwordHash := string(hash) + secret := "enabled-secret" + user := db.User{ + Username: "alice", + Email: "alice@example.com", + PasswordHash: &passwordHash, + TwoFAToken: &secret, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + result, err := userService.LoginUser(context.Background(), &dto.LoginUserRequest{ + Identifier: dto.Identifier{Identifier: "alice"}, + Password: dto.Password{Password: "Password.777"}, + }) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + if result.TwoFAPending == nil || result.TwoFAPending.SessionToken == "" { + t.Fatalf("expected 2FA pending session token") + } + }) +} + +func TestGetUserByID(t *testing.T) { + t.Run("not found", func(t *testing.T) { + userService, _ := testutil.NewTestUserService(t) + + _, err := userService.GetUserByID(context.Background(), 9999) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 404 { + t.Fatalf("expected status 404, got %d", authErr.Status) + } + }) + + t.Run("success", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user := db.User{ + Username: "bob", + Email: "bob@example.com", + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + resp, err := userService.GetUserByID(context.Background(), user.ID) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + if resp.Username != "bob" { + t.Fatalf("unexpected username: %s", resp.Username) + } + }) +} + +func TestUpdateUserPassword(t *testing.T) { + t.Run("user not found", func(t *testing.T) { + userService, _ := testutil.NewTestUserService(t) + + _, err := userService.UpdateUserPassword(context.Background(), 9999, &dto.UpdateUserPasswordRequest{ + OldPassword: dto.OldPassword{OldPassword: "Password.777"}, + NewPassword: dto.NewPassword{NewPassword: "Password.888"}, + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 404 { + t.Fatalf("expected status 404, got %d", authErr.Status) + } + }) + + t.Run("oauth user", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user := db.User{ + Username: "oauth", + Email: "oauth@example.com", + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + _, err := userService.UpdateUserPassword(context.Background(), user.ID, &dto.UpdateUserPasswordRequest{ + OldPassword: dto.OldPassword{OldPassword: "Password.777"}, + NewPassword: dto.NewPassword{NewPassword: "Password.888"}, + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 400 { + t.Fatalf("expected status 400, got %d", authErr.Status) + } + }) + + t.Run("invalid old password", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + hash, err := bcrypt.GenerateFromPassword([]byte("Password.777"), 10) + if err != nil { + t.Fatalf("failed to hash password, err: %v", err) + } + passwordHash := string(hash) + user := db.User{ + Username: "user1", + Email: "user1@example.com", + PasswordHash: &passwordHash, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + _, err = userService.UpdateUserPassword(context.Background(), user.ID, &dto.UpdateUserPasswordRequest{ + OldPassword: dto.OldPassword{OldPassword: "Wrong.777"}, + NewPassword: dto.NewPassword{NewPassword: "Password.888"}, + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 401 { + t.Fatalf("expected status 401, got %d", authErr.Status) + } + }) + + t.Run("success", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + hash, err := bcrypt.GenerateFromPassword([]byte("Password.777"), 10) + if err != nil { + t.Fatalf("failed to hash password, err: %v", err) + } + passwordHash := string(hash) + user := db.User{ + Username: "user2", + Email: "user2@example.com", + PasswordHash: &passwordHash, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + resp, err := userService.UpdateUserPassword(context.Background(), user.ID, &dto.UpdateUserPasswordRequest{ + OldPassword: dto.OldPassword{OldPassword: "Password.777"}, + NewPassword: dto.NewPassword{NewPassword: "Password.888"}, + }) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + if resp.Token == "" { + t.Fatalf("expected token in response") + } + + modelUser, err := gorm.G[db.User](myDB).Where("id = ?", user.ID).First(context.Background()) + if err != nil { + t.Fatalf("failed to query user, err: %v", err) + } + if modelUser.PasswordHash == nil { + t.Fatalf("expected password hash to be set") + } + if bcrypt.CompareHashAndPassword([]byte(*modelUser.PasswordHash), []byte("Password.888")) != nil { + t.Fatalf("expected password hash to match new password") + } + }) +} + +func TestUpdateUserProfile(t *testing.T) { + t.Run("user not found", func(t *testing.T) { + userService, _ := testutil.NewTestUserService(t) + + _, err := userService.UpdateUserProfile(context.Background(), 9999, &dto.UpdateUserRequest{ + User: dto.User{ + UserName: dto.UserName{Username: "user1"}, + Email: "user1@example.com", + }, + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 404 { + t.Fatalf("expected status 404, got %d", authErr.Status) + } + }) + + t.Run("duplicate username", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user1 := db.User{Username: "user1", Email: "user1@example.com"} + user2 := db.User{Username: "user2", Email: "user2@example.com"} + if err := gorm.G[db.User](myDB).Create(context.Background(), &user1); err != nil { + t.Fatalf("failed to create user1, err: %v", err) + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user2); err != nil { + t.Fatalf("failed to create user2, err: %v", err) + } + + _, err := userService.UpdateUserProfile(context.Background(), user2.ID, &dto.UpdateUserRequest{ + User: dto.User{ + UserName: dto.UserName{Username: "user1"}, + Email: "user2@example.com", + }, + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 409 { + t.Fatalf("expected status 409, got %d", authErr.Status) + } + }) + + t.Run("duplicate email", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user1 := db.User{Username: "user1", Email: "user1@example.com"} + user2 := db.User{Username: "user2", Email: "user2@example.com"} + if err := gorm.G[db.User](myDB).Create(context.Background(), &user1); err != nil { + t.Fatalf("failed to create user1, err: %v", err) + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user2); err != nil { + t.Fatalf("failed to create user2, err: %v", err) + } + + _, err := userService.UpdateUserProfile(context.Background(), user2.ID, &dto.UpdateUserRequest{ + User: dto.User{ + UserName: dto.UserName{Username: "user2"}, + Email: "user1@example.com", + }, + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 409 { + t.Fatalf("expected status 409, got %d", authErr.Status) + } + }) + + t.Run("avatar cleared by empty string", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + avatar := "https://example.com/a.png" + user := db.User{Username: "user1", Email: "user1@example.com", Avatar: &avatar} + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + blank := " " + _, err := userService.UpdateUserProfile(context.Background(), user.ID, &dto.UpdateUserRequest{ + User: dto.User{ + UserName: dto.UserName{Username: "user1"}, + Email: "user1@example.com", + Avatar: &blank, + }, + }) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + + modelUser, err := gorm.G[db.User](myDB).Where("id = ?", user.ID).First(context.Background()) + if err != nil { + t.Fatalf("failed to query user, err: %v", err) + } + if modelUser.Avatar != nil { + t.Fatalf("expected avatar to be cleared") + } + }) + + t.Run("success", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user := db.User{Username: "user1", Email: "user1@example.com"} + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + newAvatar := "https://example.com/new.png" + _, err := userService.UpdateUserProfile(context.Background(), user.ID, &dto.UpdateUserRequest{ + User: dto.User{ + UserName: dto.UserName{Username: "user1-updated"}, + Email: "user1-updated@example.com", + Avatar: &newAvatar, + }, + }) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + + modelUser, err := gorm.G[db.User](myDB).Where("id = ?", user.ID).First(context.Background()) + if err != nil { + t.Fatalf("failed to query user, err: %v", err) + } + if modelUser.Username != "user1-updated" || modelUser.Email != "user1-updated@example.com" { + t.Fatalf("expected user profile to be updated") + } + if modelUser.Avatar == nil || *modelUser.Avatar != newAvatar { + t.Fatalf("expected avatar to be updated") + } + }) +} + +func TestDeleteUser(t *testing.T) { + t.Run("success", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user := db.User{Username: "user1", Email: "user1@example.com"} + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + if err := userService.DeleteUser(context.Background(), user.ID); err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + + _, err := gorm.G[db.User](myDB).Where("id = ?", user.ID).First(context.Background()) + if !errors.Is(err, gorm.ErrRecordNotFound) { + t.Fatalf("expected user to be deleted") + } + }) + + t.Run("missing user", func(t *testing.T) { + userService, _ := testutil.NewTestUserService(t) + + if err := userService.DeleteUser(context.Background(), 9999); err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + }) +} + +func TestLogoutUser(t *testing.T) { + t.Run("success", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user := db.User{Username: "user1", Email: "user1@example.com"} + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + token := db.Token{ + UserID: user.ID, + Token: "token-1", + } + if err := gorm.G[db.Token](myDB).Create(context.Background(), &token); err != nil { + t.Fatalf("failed to create token, err: %v", err) + } + + if err := userService.LogoutUser(context.Background(), user.ID); err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + + _, err := gorm.G[db.Token](myDB).Where("token = ?", "token-1").First(context.Background()) + if !errors.Is(err, gorm.ErrRecordNotFound) { + t.Fatalf("expected token to be deleted") + } + }) + + t.Run("no tokens", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user := db.User{Username: "user2", Email: "user2@example.com"} + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + if err := userService.LogoutUser(context.Background(), user.ID); err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + }) +} + +func TestValidateUserToken(t *testing.T) { + t.Run("invalid token", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user := db.User{Username: "user1", Email: "user1@example.com"} + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + err := userService.ValidateUserToken(context.Background(), "nope", user.ID) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 401 { + t.Fatalf("expected status 401, got %d", authErr.Status) + } + }) + + t.Run("token does not match user", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user1 := db.User{Username: "user1", Email: "user1@example.com"} + user2 := db.User{Username: "user2", Email: "user2@example.com"} + if err := gorm.G[db.User](myDB).Create(context.Background(), &user1); err != nil { + t.Fatalf("failed to create user1, err: %v", err) + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user2); err != nil { + t.Fatalf("failed to create user2, err: %v", err) + } + + token := db.Token{ + UserID: user1.ID, + Token: "token-1", + } + if err := gorm.G[db.Token](myDB).Create(context.Background(), &token); err != nil { + t.Fatalf("failed to create token, err: %v", err) + } + + err := userService.ValidateUserToken(context.Background(), "token-1", user2.ID) + if err == nil { + t.Fatalf("expected error, got nil") + } + var authErr *authError.AuthError + if !errors.As(err, &authErr) { + t.Fatalf("expected auth error, got: %v", err) + } + if authErr.Status != 401 { + t.Fatalf("expected status 401, got %d", authErr.Status) + } + }) + + t.Run("success", func(t *testing.T) { + userService, myDB := testutil.NewTestUserService(t) + + user := db.User{Username: "user1", Email: "user1@example.com"} + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + + token := db.Token{ + UserID: user.ID, + Token: "token-1", + } + if err := gorm.G[db.Token](myDB).Create(context.Background(), &token); err != nil { + t.Fatalf("failed to create token, err: %v", err) + } + + err := userService.ValidateUserToken(context.Background(), "token-1", user.ID) + if err != nil { + t.Fatalf("unexpected error, err: %v", err) + } + }) +} diff --git a/backend/internal/testutil/testutil.go b/backend/internal/testutil/testutil.go index e5c35eb..e3ddbb5 100644 --- a/backend/internal/testutil/testutil.go +++ b/backend/internal/testutil/testutil.go @@ -1,12 +1,18 @@ package testutil import ( + "context" + "fmt" "io" "log/slog" + "strings" + "testing" "github.com/gin-gonic/gin" "github.com/paularynty/transcendence/auth-service-go/internal/config" + "github.com/paularynty/transcendence/auth-service-go/internal/db" "github.com/paularynty/transcendence/auth-service-go/internal/dependency" + "github.com/paularynty/transcendence/auth-service-go/internal/service" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) @@ -91,3 +97,53 @@ func NewIntegrationTestRouter(dep *dependency.Dependency, handlers ...gin.Handle return r } + +func GetSafeTestDBName(dbName string, testName string) string { + if !strings.Contains(dbName, "file::memory") { + return dbName + } + + safeName := strings.NewReplacer("/", "_", " ", "_").Replace(testName) + return fmt.Sprintf("file:%s?mode=memory&cache=shared", safeName) +} + +func NewTestUserService(t *testing.T) (*service.UserService, *gorm.DB) { + t.Helper() + + cfg := NewTestConfig() + logger := NewTestLogger() + + cfg.DbAddress = GetSafeTestDBName(cfg.DbAddress, t.Name()) + + myDB, err := db.GetDB(cfg.DbAddress, logger) + if err != nil { + t.Fatalf("failed to init test db, err: %v", err) + } + t.Cleanup(func() { + db.CloseDB(myDB, logger) + }) + db.ResetDB(myDB, logger) + + dep := NewTestDependency(cfg, myDB, nil, logger) + + userService, err := service.NewUserService(dep) + if err != nil { + t.Fatalf("failed to create user service, err: %v", err) + } + + return userService, myDB +} + +func CreateUser(t *testing.T, myDB *gorm.DB, username, email string, avatar *string) db.User { + t.Helper() + + user := db.User{ + Username: username, + Email: email, + Avatar: avatar, + } + if err := gorm.G[db.User](myDB).Create(context.Background(), &user); err != nil { + t.Fatalf("failed to create user, err: %v", err) + } + return user +}