From 70ddc8c93c5dbed271f099b5e3685855bb9c289b Mon Sep 17 00:00:00 2001 From: hayzam Date: Sat, 7 Sep 2024 19:09:27 +0530 Subject: [PATCH] test: auth: init tests --- services/auth.go | 108 ++++++------ services/auth_test.go | 350 ++++++++++++++++++++++++++++++++++++++ services/services_test.go | 74 +++++--- utils/strings.go | 12 ++ utils/strings_test.go | 34 ++++ 5 files changed, 503 insertions(+), 75 deletions(-) create mode 100644 services/auth_test.go diff --git a/services/auth.go b/services/auth.go index 5d1db7c..373ca24 100644 --- a/services/auth.go +++ b/services/auth.go @@ -18,6 +18,60 @@ func NewAuthService(db *gorm.DB) *AuthService { return &AuthService{DB: db} } +func (service *AuthService) GetUsers() ([]models.User, error) { + var users []models.User + + if err := service.DB.Find(&users).Error; err != nil { + return nil, fmt.Errorf("failed_to_get_users") + } + + return users, nil +} + +func (service *AuthService) CreateJWT(username, password string) (map[string]interface{}, error) { + var user models.User + + if err := service.DB.Where("username = ?", username).First(&user).Error; err != nil { + return nil, fmt.Errorf("user_not_found") + } + + if !utils.CheckPasswordHash(password, user.Password) { + return nil, fmt.Errorf("invalid_password") + } + + tokenString, expiry, err := utils.GenerateJWTAccessToken(user.ID, user.Username, user.Email, user.Photo, user.Admin, user.Permissions) + if err != nil { + return nil, fmt.Errorf("failed_to_generate_jwt") + } + + newToken := models.Token{ + UserID: user.ID, + Token: tokenString, + Expiry: expiry, + } + + if err := service.DB.Create(&newToken).Error; err != nil { + return nil, fmt.Errorf("failed_to_create_token") + } + + claims, err := utils.ValidateJWT(tokenString) + if err != nil { + service.DB.Where("token = ?", tokenString).Delete(&models.Token{}) + return nil, fmt.Errorf("invalid_jwt_created") + } + + return map[string]interface{}{ + "token": tokenString, + "expiry": claims.ExpiresAt.Time.String(), + "email": claims.Email, + "username": claims.Username, + "photo": claims.Photo, + "userId": claims.UserId, + "admin": user.Admin, + "permissions": claims.Permissions, + }, nil +} + func (service *AuthService) VerifyTokenInDb(token string, needAdmin bool) bool { var tokenRecord models.Token @@ -191,16 +245,6 @@ func (service *AuthService) DeleteUser(username string) error { return nil } -func (service *AuthService) GetUsers() ([]models.User, error) { - var users []models.User - - if err := service.DB.Find(&users).Error; err != nil { - return nil, fmt.Errorf("failed_to_get_users") - } - - return users, nil -} - func (service *AuthService) GetUser(id uint) (models.User, error) { var user models.User @@ -211,50 +255,6 @@ func (service *AuthService) GetUser(id uint) (models.User, error) { return user, nil } -func (service *AuthService) CreateJWT(username, password string) (map[string]interface{}, error) { - var user models.User - - if err := service.DB.Where("username = ?", username).First(&user).Error; err != nil { - return nil, fmt.Errorf("user_not_found") - } - - if !utils.CheckPasswordHash(password, user.Password) { - return nil, fmt.Errorf("invalid_password") - } - - tokenString, expiry, err := utils.GenerateJWTAccessToken(user.ID, user.Username, user.Email, user.Photo, user.Admin, user.Permissions) - if err != nil { - return nil, fmt.Errorf("failed_to_generate_jwt") - } - - newToken := models.Token{ - UserID: user.ID, - Token: tokenString, - Expiry: expiry, - } - - if err := service.DB.Create(&newToken).Error; err != nil { - return nil, fmt.Errorf("failed_to_create_token") - } - - claims, err := utils.ValidateJWT(tokenString) - if err != nil { - service.DB.Where("token = ?", tokenString).Delete(&models.Token{}) - return nil, fmt.Errorf("invalid_jwt_created") - } - - return map[string]interface{}{ - "token": tokenString, - "expiry": claims.ExpiresAt.Time.String(), - "email": claims.Email, - "username": claims.Username, - "photo": claims.Photo, - "userId": claims.UserId, - "admin": user.Admin, - "permissions": claims.Permissions, - }, nil -} - func (service *AuthService) CreateJWTFromEmail(email string) (string, error) { var user models.User diff --git a/services/auth_test.go b/services/auth_test.go new file mode 100644 index 0000000..71369df --- /dev/null +++ b/services/auth_test.go @@ -0,0 +1,350 @@ +package services + +import ( + "reflect" + "testing" + "time" + + "git.difuse.io/Difuse/kalmia/db/models" + "git.difuse.io/Difuse/kalmia/logger" + "git.difuse.io/Difuse/kalmia/utils" + "go.uber.org/zap" +) + +func TestGetUsers(t *testing.T) { + if TestAuthService == nil { + t.Fatal("TestAuthService is nil") + } + + users, err := TestAuthService.GetUsers() + if err != nil { + t.Fatalf("GetUsers returned an error: %v", err) + } + + expectedUsers := map[string]string{ + "admin": "admin@kalmia.difuse.io", + "user": "user@kalmia.difuse.io", + } + + if len(users) != len(expectedUsers) { + t.Errorf("Expected %d users, but got %d", len(expectedUsers), len(users)) + } + + for _, user := range users { + expectedEmail, exists := expectedUsers[user.Username] + if !exists { + t.Errorf("Unexpected user found: %s", user.Username) + continue + } + if user.Email != expectedEmail { + t.Errorf("User %s has incorrect email. Expected %s, got %s", user.Username, expectedEmail, user.Email) + } + delete(expectedUsers, user.Username) + } + + if len(expectedUsers) > 0 { + for username := range expectedUsers { + t.Errorf("Expected user not found: %s", username) + } + } +} + +func TestCreateJWT(t *testing.T) { + if TestAuthService == nil { + t.Fatal("TestAuthService is nil") + } + + t.Run("Successful JWT Creation", func(t *testing.T) { + result, err := TestAuthService.CreateJWT("admin", "admin") + if err != nil { + t.Fatalf("Failed to create JWT: %v", err) + } + + // Check if all expected fields are present + expectedFields := []string{"token", "expiry", "email", "username", "photo", "userId", "admin", "permissions"} + for _, field := range expectedFields { + if _, ok := result[field]; !ok { + t.Errorf("Expected field %s is missing from the result", field) + } + } + + // Verify some of the returned data + if result["username"] != "admin" { + t.Errorf("Expected username 'admin', got %v", result["username"]) + } + if result["email"] != "admin@kalmia.difuse.io" { + t.Errorf("Expected email 'admin@kalmia.difuse.io', got %v", result["email"]) + } + if result["admin"] != true { + t.Errorf("Expected admin to be true, got %v", result["admin"]) + } + + // Verify that the token was stored in the database + var storedToken models.Token + if err := TestAuthService.DB.Where("token = ?", result["token"]).First(&storedToken).Error; err != nil { + t.Errorf("Token not found in database: %v", err) + } + }) + + t.Run("Non-existent User", func(t *testing.T) { + _, err := TestAuthService.CreateJWT("nonexistent", "password") + if err == nil || err.Error() != "user_not_found" { + t.Errorf("Expected 'user_not_found' error, got %v", err) + } + }) + + t.Run("Incorrect Password", func(t *testing.T) { + _, err := TestAuthService.CreateJWT("admin", "wrongpassword") + if err == nil || err.Error() != "invalid_password" { + t.Errorf("Expected 'invalid_password' error, got %v", err) + } + }) +} + +func TestVerifyTokenInDb(t *testing.T) { + if TestAuthService == nil { + t.Fatal("TestAuthService is nil") + } + + createToken := func(username string) (string, error) { + result, err := TestAuthService.CreateJWT(username, username) + if err != nil { + return "", err + } + return result["token"].(string), nil + } + + t.Run("Valid Token - Non-Admin", func(t *testing.T) { + token, err := createToken("user") + if err != nil { + t.Fatalf("Failed to create token: %v", err) + } + + isValid := TestAuthService.VerifyTokenInDb(token, false) + if !isValid { + t.Errorf("Expected token to be valid, but it was not") + } + }) + + t.Run("Valid Token - Admin Check for Non-Admin", func(t *testing.T) { + token, err := createToken("user") + if err != nil { + t.Fatalf("Failed to create token: %v", err) + } + + isValid := TestAuthService.VerifyTokenInDb(token, true) + if isValid { + t.Errorf("Expected token to be invalid for admin check, but it was valid") + } + }) + + t.Run("Valid Token - Admin", func(t *testing.T) { + token, err := createToken("admin") + if err != nil { + t.Fatalf("Failed to create token: %v", err) + } + + isValid := TestAuthService.VerifyTokenInDb(token, true) + if !isValid { + t.Errorf("Expected admin token to be valid, but it was not") + } + }) + + t.Run("Invalid Token", func(t *testing.T) { + isValid := TestAuthService.VerifyTokenInDb("invalid_token", false) + if isValid { + t.Errorf("Expected invalid token to be rejected, but it was accepted") + } + }) + + t.Run("Expired Token", func(t *testing.T) { + token, err := createToken("user") + if err != nil { + t.Fatalf("Failed to create token: %v", err) + } + + err = TestAuthService.DB.Model(&models.Token{}).Where("token = ?", token).Update("expiry", time.Now().Add(-1*time.Hour)).Error + if err != nil { + t.Fatalf("Failed to expire token: %v", err) + } + + isValid := TestAuthService.VerifyTokenInDb(token, false) + if isValid { + t.Errorf("Expected expired token to be invalid, but it was valid") + } + }) +} + +func createToken(username, password string) (string, error) { + result, err := TestAuthService.CreateJWT(username, password) + if err != nil { + logger.Error("Failed to create JWT", zap.Error(err)) + return "", err + } + return result["token"].(string), nil +} + +func TestIsTokenAdmin(t *testing.T) { + if TestAuthService == nil { + t.Fatal("TestAuthService is nil") + } + + t.Run("Admin Token", func(t *testing.T) { + adminToken, err := createToken("admin", "admin") + if err != nil { + t.Fatalf("Failed to create admin token: %v", err) + } + + isAdmin := TestAuthService.IsTokenAdmin(adminToken) + if !isAdmin { + t.Errorf("Expected admin token to be identified as admin, but it was not") + } + }) + + t.Run("Non-Admin Token", func(t *testing.T) { + userToken, err := createToken("user", "user") + if err != nil { + t.Fatalf("Failed to create user token: %v", err) + } + + isAdmin := TestAuthService.IsTokenAdmin(userToken) + if isAdmin { + t.Errorf("Expected non-admin token to be identified as non-admin, but it was identified as admin") + } + }) + + t.Run("Invalid Token", func(t *testing.T) { + isAdmin := TestAuthService.IsTokenAdmin("invalid_token") + if isAdmin { + t.Errorf("Expected invalid token to be identified as non-admin, but it was identified as admin") + } + }) + + t.Run("Deleted User Token", func(t *testing.T) { + userName := "testuser" + time.Now().String() + password := "testpassword" + pwHash, err := utils.HashPassword(password) + if err != nil { + t.Fatalf("Failed to hash test password: %v", err) + } + testUser := models.User{ + Username: userName, + Email: "testuser@example.com", + Password: pwHash, + Admin: false, + Permissions: "[\"read\",\"write\",\"delete\"]", + } + if err := TestAuthService.DB.Create(&testUser).Error; err != nil { + t.Fatalf("Failed to create test user: %v", err) + } + + userToken, err := createToken(userName, password) + if err != nil { + t.Fatalf("Failed to create user token: %v", err) + } + + if err := TestAuthService.DB.Where("user_id = ?", testUser.ID).Delete(&models.Token{}).Error; err != nil { + t.Fatalf("Failed to delete associated tokens: %v", err) + } + + if err := TestAuthService.DB.Unscoped().Delete(&testUser).Error; err != nil { + t.Fatalf("Failed to delete test user: %v", err) + } + + isAdmin := TestAuthService.IsTokenAdmin(userToken) + if isAdmin { + t.Errorf("Expected token for deleted user to be identified as non-admin, but it was identified as admin") + } + }) +} + +func TestGetUserPermissions(t *testing.T) { + if TestAuthService == nil { + t.Fatal("TestAuthService is nil") + } + + t.Run("Admin Permissions", func(t *testing.T) { + adminToken, err := createToken("admin", "admin") + if err != nil { + t.Fatalf("Failed to create admin token: %v", err) + } + + permissions, err := TestAuthService.GetUserPermissions(adminToken) + if err != nil { + t.Fatalf("Failed to get admin permissions: %v", err) + } + + if len(permissions) != 1 || permissions[0] != "all" { + t.Errorf("Expected admin permissions to be [\"all\"], got %v", permissions) + } + }) + + t.Run("Regular User Permissions", func(t *testing.T) { + userToken, err := createToken("user", "user") + if err != nil { + t.Fatalf("Failed to create user token: %v", err) + } + + permissions, err := TestAuthService.GetUserPermissions(userToken) + if err != nil { + t.Fatalf("Failed to get user permissions: %v", err) + } + + expectedPermissions := []string{"read", "write", "delete"} + if !reflect.DeepEqual(permissions, expectedPermissions) { + t.Errorf("Expected user permissions to be %v, got %v", expectedPermissions, permissions) + } + }) + + t.Run("Custom User with Read Permission", func(t *testing.T) { + userName := "readonlyuser" + time.Now().String() + password := "testpassword" + pwHash, err := utils.HashPassword(password) + if err != nil { + t.Fatalf("Failed to hash test password: %v", err) + } + + testUser := models.User{ + Username: userName, + Email: "readonly@example.com", + Password: pwHash, + Admin: false, + Permissions: "[\"read\"]", + } + if err := TestAuthService.DB.Create(&testUser).Error; err != nil { + t.Fatalf("Failed to create test user: %v", err) + } + defer func() { + // Delete associated tokens first + if err := TestAuthService.DB.Where("user_id = ?", testUser.ID).Delete(&models.Token{}).Error; err != nil { + t.Fatalf("Failed to delete associated tokens: %v", err) + } + // Then delete the user + if err := TestAuthService.DB.Unscoped().Delete(&testUser).Error; err != nil { + t.Fatalf("Failed to delete test user: %v", err) + } + }() + + userToken, err := createToken(userName, password) + if err != nil { + t.Fatalf("Failed to create user token: %v", err) + } + + permissions, err := TestAuthService.GetUserPermissions(userToken) + if err != nil { + t.Fatalf("Failed to get user permissions: %v", err) + } + + expectedPermissions := []string{"read"} + if !reflect.DeepEqual(permissions, expectedPermissions) { + t.Errorf("Expected user permissions to be %v, got %v", expectedPermissions, permissions) + } + }) + + t.Run("Invalid Token", func(t *testing.T) { + _, err := TestAuthService.GetUserPermissions("invalid_token") + if err == nil { + t.Error("Expected error for invalid token, got nil") + } + }) +} diff --git a/services/services_test.go b/services/services_test.go index e1ef212..8c1aefd 100644 --- a/services/services_test.go +++ b/services/services_test.go @@ -5,39 +5,71 @@ import ( "testing" "git.difuse.io/Difuse/kalmia/config" + "git.difuse.io/Difuse/kalmia/db" "git.difuse.io/Difuse/kalmia/logger" + "git.difuse.io/Difuse/kalmia/utils" + "go.uber.org/zap" ) var TestConfig *config.Config +var TestAuthService *AuthService +var TestDocService *DocService func TestMain(m *testing.M) { - TestConfig = config.ParseConfig("../config.json") - - TestConfig.Environment = "debug" - TestConfig.Port = 3737 - TestConfig.LogLevel = "debug" - TestConfig.Database = "sqlite" - TestConfig.SessionSecret = "test-secret" - - adminUser := config.User{ - Username: "admin", - Email: "admin@kalmia.difuse.io", - Password: "admin", - Admin: true, + configJson := `{ + "environment": "debug", + "port": 3737, + "logLevel": "debug", + "database": "sqlite", + "sessionSecret": "test", + "dataPath": "./service_test_dir", + "users": [{"username": "admin", "email": "admin@kalmia.difuse.io", "password": "admin", "admin": true}, + {"username": "user", "email": "user@kalmia.difuse.io", "password": "user", "admin": false}] + }` + + err := utils.TouchFile("./config.json") + + if err != nil { + panic(err) + } + + prettyJson, err := utils.PrettyJSON(configJson) + + if err != nil { + prettyJson = configJson } - nonAdminUser := config.User{ - Username: "user", - Email: "user@kalmia.difuse.io", - Password: "user", - Admin: false, + err = utils.WriteToFile("./config.json", prettyJson) + + if err != nil { + panic(err) } - TestConfig.Admins = append(TestConfig.Admins, adminUser, nonAdminUser) + TestConfig = config.ParseConfig("./config.json") + + logger.InitializeLogger("test", TestConfig.LogLevel, TestConfig.DataPath) + + d := db.SetupDatabase(TestConfig.Environment, TestConfig.Database, TestConfig.DataPath) + db.SetupBasicData(d, TestConfig.Admins) + db.InitCache() + + serviceRegistry := NewServiceRegistry(d) + TestAuthService = serviceRegistry.AuthService + TestDocService = serviceRegistry.DocService - logger.InitializeLogger("test", "debug", "./service_test_dir") code := m.Run() - os.RemoveAll("./service_test_dir") + err = utils.RemovePath(TestConfig.DataPath) + + if err != nil { + logger.Error("Failed to remove test data path", zap.Error(err)) + } + + err = utils.RemovePath("./config.json") + + if err != nil { + logger.Error("Failed to remove test config file: %v", zap.Error(err)) + } + os.Exit(code) } diff --git a/utils/strings.go b/utils/strings.go index 7b7807d..cd5d83c 100644 --- a/utils/strings.go +++ b/utils/strings.go @@ -1,9 +1,12 @@ package utils import ( + "bytes" "crypto/sha256" "encoding/base64" "encoding/hex" + "encoding/json" + "fmt" "net/url" "path/filepath" "regexp" @@ -147,3 +150,12 @@ func HashStrings(data []string) string { h.Write([]byte(strings.Join(data, ""))) return hex.EncodeToString(h.Sum(nil)) } + +func PrettyJSON(input string) (string, error) { + var prettyJSON bytes.Buffer + err := json.Indent(&prettyJSON, []byte(input), "", " ") + if err != nil { + return "", fmt.Errorf("error formatting JSON: %v", err) + } + return prettyJSON.String(), nil +} diff --git a/utils/strings_test.go b/utils/strings_test.go index a564a92..4a1b610 100644 --- a/utils/strings_test.go +++ b/utils/strings_test.go @@ -647,3 +647,37 @@ func TestHashStrings(t *testing.T) { } } } + +func TestPrettyJSON(t *testing.T) { + tests := []struct { + input string + expected string + hasError bool + }{ + { + input: `{"name":"John","age":30,"city":"New York"}`, + expected: `{ + "name": "John", + "age": 30, + "city": "New York" +}`, + hasError: false, + }, + { + input: `{"name":"John","age":30,"city":"New York"`, // Invalid JSON + expected: "", + hasError: true, + }, + } + + for _, tt := range tests { + pretty, err := PrettyJSON(tt.input) + if (err != nil) != tt.hasError { + t.Fatalf("Expected error: %v, got: %v", tt.hasError, err) + } + + if pretty != tt.expected && !tt.hasError { + t.Fatalf("Expected:\n%s\nGot:\n%s", tt.expected, pretty) + } + } +}