diff --git a/.github/workflows/lint-test-build-golang.yml b/.github/workflows/lint-test-build-golang.yml index dba3e324..94086c76 100644 --- a/.github/workflows/lint-test-build-golang.yml +++ b/.github/workflows/lint-test-build-golang.yml @@ -113,7 +113,7 @@ jobs: - name: Run tests run: | - gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware ./tests/... -coverprofile=coverage.out + gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4.0.1 diff --git a/occupi-backend/configs/config.go b/occupi-backend/configs/config.go index 43458aff..7f5e3029 100644 --- a/occupi-backend/configs/config.go +++ b/occupi-backend/configs/config.go @@ -31,6 +31,7 @@ const ( OtpExpiration = "OTP_EXPIRATION" FrontendURL = "FRONTEND_URL" ConfigLicense = "CONFIG_LICENSE" + CacheEviction = "CACHE_EVICTION" OtpGenReqEviction = "OTP_GEN_REQ_EVICTION" AllowOriginsVal = "ALLOW_ORIGINS" AllowMethodsVal = "ALLOW_METHODS" @@ -258,6 +259,15 @@ func GetConfigLicense() string { return license } +// gets the cache eviction time as defined in the config.yaml file in seconds +func GetCacheEviction() int { + time := viper.GetInt(CacheEviction) + if time == 0 { + time = 600 + } + return time +} + // gets the otp request eviction time as defined in the config.yaml file in seconds func GetOTPReqEviction() int { time := viper.GetInt(OtpGenReqEviction) diff --git a/occupi-backend/configs/config.yaml.gpg b/occupi-backend/configs/config.yaml.gpg index 85c4ede0..7da1076c 100644 Binary files a/occupi-backend/configs/config.yaml.gpg and b/occupi-backend/configs/config.yaml.gpg differ diff --git a/occupi-backend/configs/dev.deployed.yaml.gpg b/occupi-backend/configs/dev.deployed.yaml.gpg index 87fce9d4..209286d4 100644 Binary files a/occupi-backend/configs/dev.deployed.yaml.gpg and b/occupi-backend/configs/dev.deployed.yaml.gpg differ diff --git a/occupi-backend/configs/dev.localhost.yaml.gpg b/occupi-backend/configs/dev.localhost.yaml.gpg index 7d69824e..6255cc07 100644 Binary files a/occupi-backend/configs/dev.localhost.yaml.gpg and b/occupi-backend/configs/dev.localhost.yaml.gpg differ diff --git a/occupi-backend/configs/prod.yaml.gpg b/occupi-backend/configs/prod.yaml.gpg index a1c4cda2..362fe60d 100644 Binary files a/occupi-backend/configs/prod.yaml.gpg and b/occupi-backend/configs/prod.yaml.gpg differ diff --git a/occupi-backend/configs/setup.go b/occupi-backend/configs/setup.go index 49d9d047..300148cb 100644 --- a/occupi-backend/configs/setup.go +++ b/occupi-backend/configs/setup.go @@ -56,8 +56,8 @@ func ConnectToDatabase(args ...string) *mongo.Client { // Create cache func CreateCache() *bigcache.BigCache { - config := bigcache.DefaultConfig(10 * time.Minute) // Set the eviction time to 5 seconds - config.CleanWindow = 10 * time.Minute // Set the cleanup interval to 5 seconds + config := bigcache.DefaultConfig(time.Duration(GetCacheEviction()) * time.Second) // Set the eviction time to 5 seconds + config.CleanWindow = time.Duration(GetCacheEviction()/2) * time.Second // Set the cleanup interval to 5 seconds cache, err := bigcache.New(context.Background(), config) if err != nil { logrus.Fatal(err) diff --git a/occupi-backend/configs/test.yaml.gpg b/occupi-backend/configs/test.yaml.gpg index 6f4aea5c..27a6c05b 100644 Binary files a/occupi-backend/configs/test.yaml.gpg and b/occupi-backend/configs/test.yaml.gpg differ diff --git a/occupi-backend/data/database.go b/occupi-backend/data/database.go index fc4e3456..77978cf9 100644 --- a/occupi-backend/data/database.go +++ b/occupi-backend/data/database.go @@ -85,3 +85,17 @@ func insertData(collection *mongo.Collection, documents []interface{}) { log.Printf("Collection %s already has %d documents, skipping seeding\n", collection.Name(), count) } } + +func CleanDatabase() { + // connect to the database + db := configs.ConnectToDatabase(constants.AdminDBAccessOption) + + // Drop all collections + collections := []string{"OTPS", "RoomBooking", "Rooms", "Users"} + for _, collection := range collections { + if err := db.Database(configs.GetMongoDBName()).Collection(collection).Drop(context.Background()); err != nil { + log.Fatalf("Failed to drop collection %s: %v", collection, err) + } + log.Printf("Successfully dropped collection %s\n", collection) + } +} diff --git a/occupi-backend/occupi.bat b/occupi-backend/occupi.bat index 480dc722..93510e73 100644 --- a/occupi-backend/occupi.bat +++ b/occupi-backend/occupi.bat @@ -17,7 +17,7 @@ if "%1 %2" == "run dev" ( gotestsum --format testname -- -v ./tests/... exit /b 0 ) else if "%1 %2" == "test codecov" ( - gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware ./tests/... -coverprofile=coverage.out + gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out exit /b 0 ) else if "%1" == "lint" ( golangci-lint run @@ -40,7 +40,7 @@ echo run prod : go run cmd/occupi-backend/main.go -env=dev.localhost echo build dev : go build -v cmd/occupi-backend/main.go echo build prod : go build cmd/occupi-backend/main.go echo test : gotestsum --format testname -- -v ./tests/... -echo test codecov : gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware ./tests/... -coverprofile=coverage.out +echo test codecov : gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out echo lint : golangci-lint run echo help : Show this help message exit /b 0 diff --git a/occupi-backend/occupi.sh b/occupi-backend/occupi.sh index 34ab2f50..c5bde7f0 100644 --- a/occupi-backend/occupi.sh +++ b/occupi-backend/occupi.sh @@ -9,7 +9,7 @@ print_help() { echo " build dev -> go build -v cmd/occupi-backend/main.go" echo " build prod -> go build cmd/occupi-backend/main.go" echo " test -> gotestsum --format testname -- -v ./tests/..." - echo " test codecov -> gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware ./tests/... -coverprofile=coverage.out" + echo " test codecov -> gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out" echo " lint -> golangci-lint run" echo " decrypt env -> cd scripts && chmod +x decrypt_env_variables.sh && ./decrypt_env_variables.sh" echo " encrypt env -> cd scripts && chmod +x encrypt_env_variables.sh && ./encrypt_env_variables.sh" @@ -27,7 +27,7 @@ elif [ "$1" = "build" ] && [ "$2" = "prod" ]; then elif [ "$1" = "test" ]; then gotestsum --format testname -- -v ./tests/... elif [ "$1" = "test" ] && [ "$2" = "codecov" ]; then - gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware ./tests/... -coverprofile=coverage.out + gotestsum --format testname -- -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils ./tests/... -coverprofile=coverage.out elif [ "$1" = "lint" ]; then golangci-lint run elif [ "$1" = "decrypt" ] && [ "$2" = "env" ]; then diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index c724f9dc..3fbe41e8 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -18,7 +18,11 @@ import ( ) // returns all data from the mongo database -func GetAllData(appsession *models.AppSession) []bson.M { +func GetAllData(ctx *gin.Context, appsession *models.AppSession) []bson.M { + if appsession.DB == nil { + logrus.Error("Database is nil") + return nil + } // Use the client collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") @@ -51,7 +55,7 @@ func GetAllData(appsession *models.AppSession) []bson.M { // attempts to save booking in database func SaveBooking(ctx *gin.Context, appsession *models.AppSession, booking models.Booking) (bool, error) { // Save the booking to the database - collection := appsession.DB.Database("Occupi").Collection("RoomBooking") + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("RoomBooking") _, err := collection.InsertOne(ctx, booking) if err != nil { logrus.Error(err) @@ -63,7 +67,7 @@ func SaveBooking(ctx *gin.Context, appsession *models.AppSession, booking models // Retrieves bookings associated with a user func GetUserBookings(ctx *gin.Context, appsession *models.AppSession, email string) ([]models.Booking, error) { // Get the bookings for the user - collection := appsession.DB.Database("Occupi").Collection("RoomBooking") + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("RoomBooking") filter := bson.M{ "$or": []bson.M{ {"emails": bson.M{"$elemMatch": bson.M{"$eq": email}}}, @@ -91,7 +95,7 @@ func GetUserBookings(ctx *gin.Context, appsession *models.AppSession, email stri // Confirms the user check-in by checking certain criteria func ConfirmCheckIn(ctx *gin.Context, appsession *models.AppSession, checkIn models.CheckIn) (bool, error) { // Save the check-in to the database - collection := appsession.DB.Database("Occupi").Collection("RoomBooking") + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("RoomBooking") // Find the booking by bookingId, roomId, and creator filter := bson.M{ @@ -146,19 +150,25 @@ func EmailExists(ctx *gin.Context, appsession *models.AppSession, email string) logrus.Error(err) return false } - // Add the email to the cache + + // Add the user to the cache if cache is not nil if appsession.Cache != nil { - if err := appsession.Cache.Set(email, []byte(email)); err != nil { - logrus.Error(err) + if userData, err := bson.Marshal(user); err != nil { + logrus.Error("Failed to marshall user: ", err) + } else { + if err := appsession.Cache.Set(user.Email, userData); err != nil { + logrus.Error("Failed to set key: ", err) + } } } + return true } // checks if booking exists in database func BookingExists(ctx *gin.Context, appsession *models.AppSession, id string) bool { // Check if the booking exists in the database - collection := appsession.DB.Database("Occupi").Collection("RoomBooking") + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("RoomBooking") filter := bson.M{"_id": id} var existingbooking models.Booking @@ -172,6 +182,11 @@ func BookingExists(ctx *gin.Context, appsession *models.AppSession, id string) b // adds user to database func AddUser(ctx *gin.Context, appsession *models.AppSession, user models.RequestUser) (bool, error) { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return false, errors.New("database is nil") + } // convert to user struct userStruct := models.User{ OccupiID: user.EmployeeID, @@ -189,11 +204,27 @@ func AddUser(ctx *gin.Context, appsession *models.AppSession, user models.Reques logrus.Error(err) return false, err } + // Add the user to the cache if cache is not nil + if appsession.Cache != nil { + if userData, err := bson.Marshal(userStruct); err != nil { + logrus.Error(err) + } else { + if err := appsession.Cache.Set(user.Email, userData); err != nil { + logrus.Error(err) + } + } + } + return true, nil } // adds otp to database func AddOTP(ctx *gin.Context, appsession *models.AppSession, email string, otp string) (bool, error) { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return false, errors.New("database is nil") + } // Save the OTP to the database collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("OTPS") otpStruct := models.OTP{ @@ -206,11 +237,47 @@ func AddOTP(ctx *gin.Context, appsession *models.AppSession, email string, otp s logrus.Error(err) return false, err } + // Add the OTP to the cache if cache is not nil + if appsession.Cache != nil { + if otpData, err := bson.Marshal(otpStruct); err != nil { + logrus.Error(err) + } else { + if err := appsession.Cache.Set(email+otp, otpData); err != nil { + logrus.Error(err) + } + } + } + return true, nil } // checks if otp exists in database func OTPExists(ctx *gin.Context, appsession *models.AppSession, email string, otp string) (bool, error) { + // check if otp exists in the cache if cache is not nil + if appsession.Cache != nil { + // unmarshal the user from the cache + var otpStruct models.OTP + otpData, err := appsession.Cache.Get(email + otp) + if err != nil { + logrus.Error("key does not exist: ", err) + } else { + if err := bson.Unmarshal(otpData, &otpStruct); err != nil { + logrus.Error("failed to unmarshall", err) + } else { + if time.Now().After(otpStruct.ExpireWhen) { + return false, nil + } + return true, nil + } + } + } + + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return false, errors.New("database is nil") + } + // Check if the OTP exists in the database collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("OTPS") filter := bson.M{"email": email, "otp": otp} @@ -220,15 +287,33 @@ func OTPExists(ctx *gin.Context, appsession *models.AppSession, email string, ot logrus.Error(err) return false, err } + // Check if the OTP has expired if time.Now().After(otpStruct.ExpireWhen) { return false, nil } + + // Add the OTP to the cache if cache is not nil + if appsession.Cache != nil { + if otpData, err := bson.Marshal(otpStruct); err != nil { + logrus.Error(err) + } else { + if err := appsession.Cache.Set(email+otp, otpData); err != nil { + logrus.Error(err) + } + } + } + return true, nil } // deletes otp from database func DeleteOTP(ctx *gin.Context, appsession *models.AppSession, email string, otp string) (bool, error) { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return false, errors.New("database is nil") + } // Delete the OTP from the database collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("OTPS") filter := bson.M{"email": email, "otp": otp} @@ -237,24 +322,37 @@ func DeleteOTP(ctx *gin.Context, appsession *models.AppSession, email string, ot logrus.Error(err) return false, err } + + // delete otp from cache if cache is not nil + if appsession.Cache != nil { + if err := appsession.Cache.Delete(email + otp); err != nil { + logrus.Error(err) // the otp may also not xist in the cache which is valid behaviour + } + } + return true, nil } // GetResetOTP retrieves the OTP for the given email and OTP from the database func GetResetOTP(ctx context.Context, db *mongo.Client, email, otp string) (*models.OTP, error) { - collection := db.Database("Occupi").Collection("OTPs") - var resetOTP models.OTP - filter := bson.M{"email": email, "otp": otp} - err := collection.FindOne(ctx, filter).Decode(&resetOTP) - if err != nil { - return nil, err - } - return &resetOTP, nil + collection := db.Database(configs.GetMongoDBName()).Collection("OTPs") + var resetOTP models.OTP + filter := bson.M{"email": email, "otp": otp} + err := collection.FindOne(ctx, filter).Decode(&resetOTP) + if err != nil { + return nil, err + } + return &resetOTP, nil } - // verifies a user in the database func VerifyUser(ctx *gin.Context, appsession *models.AppSession, email string) (bool, error) { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return false, errors.New("database is nil") + } + // Verify the user in the database and set next date to verify to 30 days from now collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") filter := bson.M{"email": email} @@ -264,11 +362,58 @@ func VerifyUser(ctx *gin.Context, appsession *models.AppSession, email string) ( logrus.Error(err) return false, err } + + // if user is in cache, update the user in the cache + if appsession.Cache == nil { + return true, nil + } + + var user models.User + userData, err := appsession.Cache.Get(email) + if err != nil { + logrus.Error(err) + return true, nil + } + + if err := bson.Unmarshal(userData, &user); err != nil { + logrus.Error(err) + return true, nil + } + + user.IsVerified = true + user.NextVerificationDate = time.Now().AddDate(0, 0, 30) + if userData, err := bson.Marshal(user); err == nil { + if err := appsession.Cache.Set(email, userData); err != nil { + logrus.Error(err) + } + } + return true, nil } // get's the hash password stored in the database belonging to this user func GetPassword(ctx *gin.Context, appsession *models.AppSession, email string) (string, error) { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return "", errors.New("database is nil") + } + + // Get the password from the cache if cache is not nil + if appsession.Cache != nil { + // unmarshal the user from the cache + var user models.User + userData, err := appsession.Cache.Get(email) + if err != nil { + logrus.Error("key does not exist: ", err) + } else { + if err := bson.Unmarshal(userData, &user); err != nil { + logrus.Error("failed to unmarshall", err) + } else { + return user.Password, nil + } + } + } // Get the password from the database collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") filter := bson.M{"email": email} @@ -278,11 +423,53 @@ func GetPassword(ctx *gin.Context, appsession *models.AppSession, email string) logrus.Error(err) return "", err } + + // Add the user to the cache if cache is not nil + if appsession.Cache != nil { + if userData, err := bson.Marshal(user); err != nil { + logrus.Error(err) + } else { + if err := appsession.Cache.Set(user.Email, userData); err != nil { + logrus.Error(err) + } + } + } + return user.Password, nil } // checks if the next verification date is due func CheckIfNextVerificationDateIsDue(ctx *gin.Context, appsession *models.AppSession, email string) (bool, error) { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return false, errors.New("database is nil") + } + + // Get the user from the cache if cache is not nil + if appsession.Cache != nil { + // unmarshal the user from the cache + var user models.User + userData, err := appsession.Cache.Get(email) + if err != nil { + logrus.Error(err) + } else { + if err := bson.Unmarshal(userData, &user); err != nil { + logrus.Error(err) + } else { + if time.Now().After(user.NextVerificationDate) { + _, err := UpdateVerificationStatusTo(ctx, appsession, email, false) + if err != nil { + logrus.Error(err) + return false, err + } + return true, nil + } + return false, nil + } + } + } + // Check if the next verification date is due collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") filter := bson.M{"email": email} @@ -292,6 +479,18 @@ func CheckIfNextVerificationDateIsDue(ctx *gin.Context, appsession *models.AppSe logrus.Error(err) return false, err } + + // Add the user to the cache if cache is not nil + if appsession.Cache != nil { + if userData, err := bson.Marshal(user); err != nil { + logrus.Error(err) + } else { + if err := appsession.Cache.Set(user.Email, userData); err != nil { + logrus.Error(err) + } + } + } + if time.Now().After(user.NextVerificationDate) { _, err := UpdateVerificationStatusTo(ctx, appsession, email, false) if err != nil { @@ -305,6 +504,28 @@ func CheckIfNextVerificationDateIsDue(ctx *gin.Context, appsession *models.AppSe // checks if the user is verified func CheckIfUserIsVerified(ctx *gin.Context, appsession *models.AppSession, email string) (bool, error) { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return false, errors.New("database is nil") + } + + // Check if the user is verified in the cache if cache is not nil + if appsession.Cache != nil { + // unmarshal the user from the cache + var user models.User + userData, err := appsession.Cache.Get(email) + if err != nil { + logrus.Error(err) + } else { + if err := bson.Unmarshal(userData, &user); err != nil { + logrus.Error(err) + } else { + return user.IsVerified, nil + } + } + } + // Check if the user is verified collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") filter := bson.M{"email": email} @@ -314,11 +535,29 @@ func CheckIfUserIsVerified(ctx *gin.Context, appsession *models.AppSession, emai logrus.Error(err) return false, err } + + // Add the user to the cache if cache is not nil + if appsession.Cache != nil { + if userData, err := bson.Marshal(user); err != nil { + logrus.Error(err) + } else { + if err := appsession.Cache.Set(user.Email, userData); err != nil { + logrus.Error(err) + } + } + } + return user.IsVerified, nil } // updates the users verification status to true or false func UpdateVerificationStatusTo(ctx *gin.Context, appsession *models.AppSession, email string, status bool) (bool, error) { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return false, errors.New("database is nil") + } + // Update the verification status of the user collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") filter := bson.M{"email": email} @@ -328,13 +567,39 @@ func UpdateVerificationStatusTo(ctx *gin.Context, appsession *models.AppSession, logrus.Error(err) return false, err } + + // if user is in cache, update the user in the cache + if appsession.Cache == nil { + logrus.Error("Cache is nil") + return true, nil + } + + var user models.User + userData, err := appsession.Cache.Get(email) + if err != nil { + logrus.Error(err) + return true, nil + } + + if err := bson.Unmarshal(userData, &user); err != nil { + logrus.Error(err) + return true, nil + } + + user.IsVerified = status + if userData, err := bson.Marshal(user); err == nil { + if err := appsession.Cache.Set(email, userData); err != nil { + logrus.Error(err) + } + } + return true, nil } // Confirms if a booking has been cancelled func ConfirmCancellation(ctx *gin.Context, appsession *models.AppSession, id string, email string) (bool, error) { // Save the check-in to the database - collection := appsession.DB.Database("Occupi").Collection("RoomBooking") + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("RoomBooking") // Find the booking by bookingId, roomId, and check if the email is in the emails object filter := bson.M{ @@ -364,7 +629,7 @@ func ConfirmCancellation(ctx *gin.Context, appsession *models.AppSession, id str // Gets all rooms available for booking func GetAllRooms(ctx *gin.Context, appsession *models.AppSession, floorNo string) ([]models.Room, error) { - collection := appsession.DB.Database("Occupi").Collection("Rooms") + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Rooms") var cursor *mongo.Cursor var err error @@ -404,7 +669,7 @@ func GetAllRooms(ctx *gin.Context, appsession *models.AppSession, floorNo string // Get user information func GetUserDetails(ctx *gin.Context, appsession *models.AppSession, email string) (models.UserDetails, error) { - collection := appsession.DB.Database("Occupi").Collection("Users") + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") filter := bson.M{"email": email} var user models.UserDetails @@ -469,7 +734,7 @@ func AddFieldToUpdateMap(updateFields bson.M, fieldName string, fieldValue inter // UpdateUserDetails updates the user's details func UpdateUserDetails(ctx *gin.Context, appsession *models.AppSession, user models.UserDetails) (bool, error) { - collection := appsession.DB.Database("Occupi").Collection("Users") + collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") filter := bson.M{"email": user.Email} var userStruct models.UserDetails @@ -504,6 +769,28 @@ func UpdateUserDetails(ctx *gin.Context, appsession *models.AppSession, user mod // Checks if a user is an admin func CheckIfUserIsAdmin(ctx *gin.Context, appsession *models.AppSession, email string) (bool, error) { + // check if database is nil + if appsession.DB == nil { + logrus.Error("Database is nil") + return false, errors.New("database is nil") + } + + // Get the user from the cache if cache is not nil + if appsession.Cache != nil { + // unmarshal the user from the cache + var user models.User + userData, err := appsession.Cache.Get(email) + if err != nil { + logrus.Error(err) + } else { + if err := bson.Unmarshal(userData, &user); err != nil { + logrus.Error(err) + } else { + return user.Role == constants.Admin, nil + } + } + } + // Check if the user is an admin collection := appsession.DB.Database(configs.GetMongoDBName()).Collection("Users") filter := bson.M{"email": email} @@ -513,88 +800,108 @@ func CheckIfUserIsAdmin(ctx *gin.Context, appsession *models.AppSession, email s logrus.Error(err) return false, err } + + // Add the user to the cache if cache is not nil + if appsession.Cache != nil { + if userData, err := bson.Marshal(user); err != nil { + logrus.Error(err) + } else { + if err := appsession.Cache.Set(user.Email, userData); err != nil { + logrus.Error(err) + } + } + } + return user.Role == constants.Admin, nil } - // AddResetToken adds a reset token to the database func AddResetToken(ctx context.Context, db *mongo.Client, email string, resetToken string, expirationTime time.Time) (bool, error) { - collection := db.Database("Occupi").Collection("ResetTokens") - resetTokenStruct := models.ResetToken{ - Email: email, - Token: resetToken, - ExpireWhen: expirationTime, - } - _, err := collection.InsertOne(ctx, resetTokenStruct) - if err != nil { - logrus.Error(err) - return false, err - } - return true, nil + collection := db.Database(configs.GetMongoDBName()).Collection("ResetTokens") + resetTokenStruct := models.ResetToken{ + Email: email, + Token: resetToken, + ExpireWhen: expirationTime, + } + _, err := collection.InsertOne(ctx, resetTokenStruct) + if err != nil { + logrus.Error(err) + return false, err + } + return true, nil } // retrieves the email associated with a reset token func GetEmailByResetToken(ctx context.Context, db *mongo.Client, resetToken string) (string, error) { - collection := db.Database("Occupi").Collection("ResetTokens") - filter := bson.M{"token": resetToken} - var resetTokenStruct models.ResetToken - err := collection.FindOne(ctx, filter).Decode(&resetTokenStruct) - if err != nil { - logrus.Error(err) - return "", err - } - return resetTokenStruct.Email, nil -} - -// CheckResetToken function + collection := db.Database(configs.GetMongoDBName()).Collection("ResetTokens") + filter := bson.M{"token": resetToken} + var resetTokenStruct models.ResetToken + err := collection.FindOne(ctx, filter).Decode(&resetTokenStruct) + if err != nil { + logrus.Error(err) + return "", err + } + return resetTokenStruct.Email, nil +} + +// CheckResetToken function func CheckResetToken(ctx *gin.Context, db *mongo.Client, email string, token string) (bool, error) { - // Access the "ResetTokens" collection within the "Occupi" database. - collection := db.Database("Occupi").Collection("ResetTokens") - - // Create a filter to find the document matching the provided email and token. - filter := bson.M{"email": email, "token": token} - - // Define a variable to hold the reset token document. - var resetToken models.ResetToken - - // Attempt to find the document in the collection. - err := collection.FindOne(ctx, filter).Decode(&resetToken) - if err != nil { - // Log and return the error if the document cannot be found or decoded. - logrus.Error(err) - return false, err - } - - // Check if the current time is after the token's expiration time. - if time.Now().After(resetToken.ExpireWhen) { - // Return false indicating the token has expired. - return false, nil - } - - // Return true indicating the token is still valid. - return true, nil + // Access the "ResetTokens" collection within the configs.GetMongoDBName() database. + collection := db.Database(configs.GetMongoDBName()).Collection("ResetTokens") + + // Create a filter to find the document matching the provided email and token. + filter := bson.M{"email": email, "token": token} + + // Define a variable to hold the reset token document. + var resetToken models.ResetToken + + // Attempt to find the document in the collection. + err := collection.FindOne(ctx, filter).Decode(&resetToken) + if err != nil { + // Log and return the error if the document cannot be found or decoded. + logrus.Error(err) + return false, err + } + + // Check if the current time is after the token's expiration time. + if time.Now().After(resetToken.ExpireWhen) { + // Return false indicating the token has expired. + return false, nil + } + + // Return true indicating the token is still valid. + return true, nil } // UpdateUserPassword, which updates the password in the database set by the user func UpdateUserPassword(ctx *gin.Context, db *mongo.Client, email string, password string) (bool, error) { + // Check if the database is nil + if db == nil { + logrus.Error("Database is nil") + return false, errors.New("database is nil") + } + // Update the password in the database - collection := db.Database("Occupi").Collection("Users") + collection := db.Database(configs.GetMongoDBName()).Collection("Users") filter := bson.M{"email": email} update := bson.M{"$set": bson.M{"password": password}} - _, err := collection.UpdateOne(ctx, filter,update) + _, err := collection.UpdateOne(ctx, filter, update) if err != nil { logrus.Error(err) return false, err } + + // Update users password in cache if cache is not nil + return true, nil } // ClearRestToekn, removes the reset token from the database func ClearResetToken(ctx *gin.Context, db *mongo.Client, email string, token string) (bool, error) { // Delete the token from the database - collection := db.Database("Occupi").Collection("ResetTokens") + collection := db.Database(configs.GetMongoDBName()).Collection("ResetTokens") filter := bson.M{"email": email, "token": token} - _, err := collection.DeleteOne(ctx,filter) + _, err := collection.DeleteOne(ctx, filter) if err != nil { logrus.Error(err) return false, err @@ -602,78 +909,78 @@ func ClearResetToken(ctx *gin.Context, db *mongo.Client, email string, token str return true, nil } -// ValidateResetToken +// ValidateResetToken func ValidateResetToken(ctx context.Context, db *mongo.Client, email, token string) (bool, string, error) { - // Find the reset token document - var resetToken models.ResetToken - collection := db.Database("Occupi").Collection("ResetTokens") - err := collection.FindOne(ctx, bson.M{"email": email, "token": token}).Decode(&resetToken) - if err != nil { - if err == mongo.ErrNoDocuments { - return false, "Invalid or expired token", nil - } - return false, "", err - } + // Find the reset token document + var resetToken models.ResetToken + collection := db.Database(configs.GetMongoDBName()).Collection("ResetTokens") + err := collection.FindOne(ctx, bson.M{"email": email, "token": token}).Decode(&resetToken) + if err != nil { + if err == mongo.ErrNoDocuments { + return false, "Invalid or expired token", nil + } + return false, "", err + } - // Check if the token has expired - if time.Now().After(resetToken.ExpireWhen) { - return false, "Token has expired", nil - } + // Check if the token has expired + if time.Now().After(resetToken.ExpireWhen) { + return false, "Token has expired", nil + } - return true, "", nil + return true, "", nil } // SaveTwoFACode saves the 2FA code for a user func SaveTwoFACode(ctx context.Context, db *mongo.Client, email, code string) error { - collection := db.Database("Occupi").Collection("Users") - filter := bson.M{"email": email} - update := bson.M{ - "$set": bson.M{ - "twoFACode": code, - "twoFACodeExpiry": time.Now().Add(10 * time.Minute), - }, - } - _, err := collection.UpdateOne(ctx, filter, update) - return err + collection := db.Database(configs.GetMongoDBName()).Collection("Users") + filter := bson.M{"email": email} + update := bson.M{ + "$set": bson.M{ + "twoFACode": code, + "twoFACodeExpiry": time.Now().Add(10 * time.Minute), + }, + } + _, err := collection.UpdateOne(ctx, filter, update) + return err } // VerifyTwoFACode checks if the provided 2FA code is valid for the user func VerifyTwoFACode(ctx context.Context, db *mongo.Client, email, code string) (bool, error) { - collection := db.Database("Occupi").Collection("Users") - filter := bson.M{ - "email": email, - "twoFACode": code, - "twoFACodeExpiry": bson.M{"$gt": time.Now()}, - } - var user models.User - err := collection.FindOne(ctx, filter).Decode(&user) - if err != nil { - if err == mongo.ErrNoDocuments { - return false, nil - } - return false, err - } - return true, nil + collection := db.Database(configs.GetMongoDBName()).Collection("Users") + filter := bson.M{ + "email": email, + "twoFACode": code, + "twoFACodeExpiry": bson.M{"$gt": time.Now()}, + } + var user models.User + err := collection.FindOne(ctx, filter).Decode(&user) + if err != nil { + if err == mongo.ErrNoDocuments { + return false, nil + } + return false, err + } + return true, nil } // IsTwoFAEnabled checks if 2FA is enabled for the user func IsTwoFAEnabled(ctx context.Context, db *mongo.Client, email string) (bool, error) { - collection := db.Database("Occupi").Collection("Users") - filter := bson.M{"email": email} - var user models.User - err := collection.FindOne(ctx, filter).Decode(&user) - if err != nil { - return false, err - } + collection := db.Database(configs.GetMongoDBName()).Collection("Users") + filter := bson.M{"email": email} + var user models.User + err := collection.FindOne(ctx, filter).Decode(&user) + if err != nil { + return false, err + } return user.TwoFAEnabled, nil } -// setting the 2fa enabled +// setting the 2fa enabled func SetTwoFAEnabled(ctx context.Context, db *mongo.Database, email string, enabled bool) error { - collection := db.Collection("users") - filter := bson.M{"email": email} - update := bson.M{"$set": bson.M{"twoFAEnabled": enabled}} + collection := db.Collection("users") + filter := bson.M{"email": email} + update := bson.M{"$set": bson.M{"twoFAEnabled": enabled}} - _, err := collection.UpdateOne(ctx, filter, update) - return err + _, err := collection.UpdateOne(ctx, filter, update) + return err } diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index aa7e686c..6bf77074 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -40,14 +40,14 @@ func PingHandlerAdmin(ctx *gin.Context) { // handler for fetching test resource from /api/resource. Formats and returns json response func FetchResource(ctx *gin.Context, appsession *models.AppSession) { - data := database.GetAllData(appsession) + data := database.GetAllData(ctx, appsession) ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Data fetched successfully", data)) } // handler for fetching test resource from /api/resource. Formats and returns json response func FetchResourceAuth(ctx *gin.Context, appsession *models.AppSession) { - data := database.GetAllData(appsession) + data := database.GetAllData(ctx, appsession) ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Data fetched successfully", data)) } diff --git a/occupi-backend/pkg/handlers/auth_handlers.go b/occupi-backend/pkg/handlers/auth_handlers.go index ba7825dc..ffd276d0 100644 --- a/occupi-backend/pkg/handlers/auth_handlers.go +++ b/occupi-backend/pkg/handlers/auth_handlers.go @@ -708,6 +708,7 @@ func ResetPassword(ctx *gin.Context, appsession *models.AppSession) { var request struct { Email string `json:"email" binding:"required,email"` } + if err := ctx.ShouldBindBodyWithJSON(&request); err != nil { ctx.JSON(http.StatusBadRequest, utils.ErrorResponse( http.StatusBadRequest, @@ -738,7 +739,6 @@ func ForgotPassword(ctx *gin.Context, appsession *models.AppSession) { handlePasswordReset(ctx, appsession, request.Email) } - // handler for logging out a user func Logout(ctx *gin.Context) { session := sessions.Default(ctx) diff --git a/occupi-backend/tests/cache_test.go b/occupi-backend/tests/cache_test.go index b892a25b..eec95626 100644 --- a/occupi-backend/tests/cache_test.go +++ b/occupi-backend/tests/cache_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" "go.mongodb.org/mongo-driver/bson" "github.com/COS301-SE-2024/occupi/occupi-backend/configs" @@ -33,7 +34,7 @@ func TestEmailExistsPerformance(t *testing.T) { appsessionWithoutCache := models.New(db, nil) // Mock the DB response - collection := db.Database("Occupi").Collection("Users") + collection := db.Database(configs.GetMongoDBName()).Collection("Users") _, err := collection.InsertOne(ctx, bson.M{"email": email}) if err != nil { t.Fatalf("Failed to insert test email into database: %v", err) @@ -58,3 +59,246 @@ func TestEmailExistsPerformance(t *testing.T) { t.Errorf("Cache did not improve performance: duration with cache %v, duration without cache %v", durationWithCache, durationWithoutCache) } } + +func TestEmailExists_WithCache(t *testing.T) { + email := "test@example.com" + // Create database connection and cache + db := configs.ConnectToDatabase(constants.AdminDBAccessOption) + cache := configs.CreateCache() + + // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + w := httptest.NewRecorder() + + // Create a response writer and context + ctx, _ := gin.CreateTestContext(w) + + // Create a new AppSession with the cache + appSession := models.New(db, cache) + + // Mock the DB response + collection := db.Database(configs.GetMongoDBName()).Collection("Users") + _, err := collection.InsertOne(ctx, bson.M{"email": email}) + if err != nil { + t.Fatalf("Failed to insert test email into database: %v", err) + } + + // call the function to test + exists := database.EmailExists(ctx, appSession, email) + + // Verify the response + assert.True(t, exists) + + // Verify the user is in the cache + cachedUser, err := cache.Get(email) + + assert.Nil(t, err) + assert.NotNil(t, cachedUser) +} + +func TestAddUser_WithCache(t *testing.T) { + // Create database connection and cache + db := configs.ConnectToDatabase(constants.AdminDBAccessOption) + cache := configs.CreateCache() + + // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + w := httptest.NewRecorder() + + // Create a response writer and context + ctx, _ := gin.CreateTestContext(w) + + // Create a new AppSession with the cache + appSession := models.New(db, cache) + + user := models.RequestUser{ + Email: "test_withcache@example.com", + Password: "password", + EmployeeID: "12345", + } + + success, err := database.AddUser(ctx, appSession, user) + assert.True(t, success) + assert.Nil(t, err) + + // Verify the user is in the cache + cachedUser, err := cache.Get(user.Email) + assert.Nil(t, err) + assert.NotNil(t, cachedUser) + + // sleep for 2 * cache expiry time to ensure the cache expires + time.Sleep(time.Duration(configs.GetCacheEviction()) * 2 * time.Second) + + // Verify the user is not in the cache + cachedUser, err = cache.Get(user.Email) + assert.NotNil(t, err) + assert.Nil(t, cachedUser) +} + +func TestAddOTP_WithCache(t *testing.T) { + // Create database connection and cache + db := configs.ConnectToDatabase(constants.AdminDBAccessOption) + cache := configs.CreateCache() + + // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + w := httptest.NewRecorder() + + // Create a response writer and context + ctx, _ := gin.CreateTestContext(w) + + // Create a new AppSession with the cache + appSession := models.New(db, cache) + + email := "test_withcache@example.com" + otp := "123456" + + success, err := database.AddOTP(ctx, appSession, email, otp) + assert.True(t, success) + assert.Nil(t, err) + + // Verify the otp is in the cache + cachedUser, err := cache.Get(email + otp) + assert.Nil(t, err) + assert.NotNil(t, cachedUser) + + // sleep for 2 * cache expiry time to ensure the cache expires + time.Sleep(time.Duration(configs.GetCacheEviction()) * 2 * time.Second) + + // Verify the user is not in the cache + cachedUser, err = cache.Get(email + otp) + assert.NotNil(t, err) + assert.Nil(t, cachedUser) +} + +func TestOTPExistsPerformance(t *testing.T) { + email := "test@example.com" + otp := "123456" + + // Create database connection and cache + db := configs.ConnectToDatabase(constants.AdminDBAccessOption) + cache := configs.CreateCache() + + // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + w := httptest.NewRecorder() + + // Create a response writer and context + ctx, _ := gin.CreateTestContext(w) + + // Create a new AppSession with the cache + appsessionWithCache := models.New(db, cache) + // Create a new AppSession without the cache + appsessionWithoutCache := models.New(db, nil) + + // Mock the DB response + collection := db.Database(configs.GetMongoDBName()).Collection("OTPS") + otpStruct := models.OTP{ + Email: email, + OTP: otp, + ExpireWhen: time.Now().Add(time.Second * time.Duration(configs.GetOTPExpiration())), + } + _, err := collection.InsertOne(ctx, otpStruct) + if err != nil { + t.Fatalf("Failed to insert test otp into database: %v", err) + } + + // Test performance with cache + startTime := time.Now() + for i := 0; i < 1000; i++ { + database.OTPExists(ctx, appsessionWithCache, email, otp) + } + durationWithCache := time.Since(startTime) + + // Test performance without cache + startTime = time.Now() + for i := 0; i < 1000; i++ { + database.OTPExists(ctx, appsessionWithoutCache, email, otp) + } + durationWithoutCache := time.Since(startTime) + + // Assert that the cache improves the speed + if durationWithoutCache <= durationWithCache { + t.Errorf("Cache did not improve performance: duration with cache %v, duration without cache %v", durationWithCache, durationWithoutCache) + } +} + +func TestOTPExists_WithCache(t *testing.T) { + email := "test@example.com" + otp := "123456" + // Create database connection and cache + db := configs.ConnectToDatabase(constants.AdminDBAccessOption) + cache := configs.CreateCache() + + // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + w := httptest.NewRecorder() + + // Create a response writer and context + ctx, _ := gin.CreateTestContext(w) + + // Create a new AppSession with the cache + appSession := models.New(db, cache) + + // Mock the DB response + collection := db.Database(configs.GetMongoDBName()).Collection("OTPS") + otpStruct := models.OTP{ + Email: email, + OTP: otp, + ExpireWhen: time.Now().Add(time.Second * time.Duration(configs.GetOTPExpiration())), + } + _, err := collection.InsertOne(ctx, otpStruct) + if err != nil { + t.Fatalf("Failed to insert test otp into database: %v", err) + } + + // call the function to test + exists, err := database.OTPExists(ctx, appSession, email, otp) + + // Verify the response + assert.True(t, exists) + assert.Nil(t, err) + + // Verify the user is in the cache + cachedUser, err := cache.Get(email + otp) + + assert.Nil(t, err) + assert.NotNil(t, cachedUser) +} + +func TestDeleteOTP_withCache(t *testing.T) { + email := "test@example.com" + otp := "123456" + // Create database connection and cache + db := configs.ConnectToDatabase(constants.AdminDBAccessOption) + cache := configs.CreateCache() + + // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + w := httptest.NewRecorder() + + // Create a response writer and context + ctx, _ := gin.CreateTestContext(w) + + // Create a new AppSession with the cache + appSession := models.New(db, cache) + + // Mock the DB response + collection := db.Database(configs.GetMongoDBName()).Collection("OTPS") + otpStruct := models.OTP{ + Email: email, + OTP: otp, + ExpireWhen: time.Now().Add(time.Second * time.Duration(configs.GetOTPExpiration())), + } + _, err := collection.InsertOne(ctx, otpStruct) + if err != nil { + t.Fatalf("Failed to insert test otp into database: %v", err) + } + + // call the function to test + success, err := database.DeleteOTP(ctx, appSession, email, otp) + + // Verify the response + assert.True(t, success) + assert.Nil(t, err) + + // Verify the user is in the cache + cachedUser, err := cache.Get(email + otp) + + assert.NotNil(t, err) + assert.Nil(t, cachedUser) +} diff --git a/occupi-backend/tests/database_test.go b/occupi-backend/tests/database_test.go index d1c24b41..ba743caa 100644 --- a/occupi-backend/tests/database_test.go +++ b/occupi-backend/tests/database_test.go @@ -69,6 +69,21 @@ func TestGetAllData(t *testing.T) { // Setup mock MongoDB instance mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) + gin.SetMode(configs.GetGinRunMode()) + + // Create a new HTTP request with the POST method. + req, _ := http.NewRequest("POST", "/", nil) + + // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + w := httptest.NewRecorder() + + // Create a new context with the Request and ResponseWriter. + ctx, _ := gin.CreateTestContext(w) + ctx.Request = req + + // Optionally, set any values in the context. + ctx.Set("test", "test") + // Define the mock responses onSiteTrueDocs := []bson.D{ {{Key: "onSite", Value: true}, {Key: "name", Value: "User1"}}, @@ -79,7 +94,7 @@ func TestGetAllData(t *testing.T) { mt.AddMockResponses(mtest.CreateCursorResponse(1, "Occupi.Users", mtest.FirstBatch, onSiteTrueDocs...)) // Call the function under test - users := database.GetAllData(models.New(mt.Client, nil)) + users := database.GetAllData(ctx, models.New(mt.Client, nil)) // Validate the result expected := []bson.M{ @@ -112,6 +127,14 @@ func TestEmailExists(t *testing.T) { email := "test@example.com" + mt.Run("Nil database", func(mt *mtest.T) { + // Call the function under test + exists := database.EmailExists(ctx, models.New(nil, nil), email) + + // Validate the result + assert.False(t, exists) + }) + mt.Run("Email exists", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateCursorResponse(1, "Occupi.Users", mtest.FirstBatch, bson.D{ {Key: "email", Value: email}, @@ -173,6 +196,15 @@ func TestAddUser(t *testing.T) { Email: "test@example.com", } + mt.Run("Nil database", func(mt *mtest.T) { + // Call the function under test + success, err := database.AddUser(ctx, models.New(nil, nil), user) + + // Validate the result + assert.Error(t, err) + assert.False(t, success) + }) + mt.Run("Add user successfully", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateSuccessResponse()) @@ -223,6 +255,15 @@ func TestOTPExists(t *testing.T) { expiredOTP := time.Now().Add(-1 * time.Hour) validOTP := time.Now().Add(1 * time.Hour) + mt.Run("Nil database", func(mt *mtest.T) { + // Call the function under test + exists, err := database.OTPExists(ctx, models.New(nil, nil), email, otp) + + // Validate the result + assert.Error(t, err) + assert.False(t, exists) + }) + mt.Run("OTP exists and is valid", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateCursorResponse(1, "Occupi.OTPS", mtest.FirstBatch, bson.D{ {Key: "email", Value: email}, @@ -301,6 +342,15 @@ func TestAddOTP(t *testing.T) { email := "test@example.com" otp := "123456" + mt.Run("Nil database", func(mt *mtest.T) { + // Call the function under test + success, err := database.AddOTP(ctx, models.New(nil, nil), email, otp) + + // Validate the result + assert.Error(t, err) + assert.False(t, success) + }) + mt.Run("Add OTP successfully", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateSuccessResponse()) @@ -351,6 +401,15 @@ func TestDeleteOTP(t *testing.T) { email := "test@example.com" otp := "123456" + mt.Run("Nil database", func(mt *mtest.T) { + // Call the function under test + success, err := database.DeleteOTP(ctx, models.New(nil, nil), email, otp) + + // Validate the result + assert.Error(t, err) + assert.False(t, success) + }) + mt.Run("Delete OTP successfully", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateSuccessResponse()) @@ -400,6 +459,15 @@ func TestVerifyUser(t *testing.T) { email := "test@example.com" + mt.Run("Nil database", func(mt *mtest.T) { + // Call the function under test + success, err := database.VerifyUser(ctx, models.New(nil, nil), email) + + // Validate the result + assert.Error(t, err) + assert.False(t, success) + }) + mt.Run("Verify user successfully", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateSuccessResponse()) @@ -450,6 +518,15 @@ func TestGetPassword(t *testing.T) { email := "test@example.com" password := "hashedpassword123" + mt.Run("Nil database", func(mt *mtest.T) { + // Call the function under test + pass, err := database.GetPassword(ctx, models.New(nil, nil), email) + + // Validate the result + assert.Error(t, err) + assert.Equal(t, "", pass) + }) + mt.Run("Get password successfully", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateCursorResponse(1, "Occupi.Users", mtest.FirstBatch, bson.D{ {Key: "email", Value: email}, @@ -500,6 +577,15 @@ func TestCheckIfUserIsVerified(t *testing.T) { email := "test@example.com" + mt.Run("Nil database", func(mt *mtest.T) { + // Call the function under test + isVerified, err := database.CheckIfUserIsVerified(ctx, models.New(nil, nil), email) + + // Validate the result + assert.Error(t, err) + assert.False(t, isVerified) + }) + mt.Run("User is verified", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateCursorResponse(1, "Occupi.Users", mtest.FirstBatch, bson.D{ {Key: "email", Value: email}, @@ -564,6 +650,15 @@ func TestUpdateVerificationStatusTo(t *testing.T) { email := "test@example.com" + mt.Run("Nil database", func(mt *mtest.T) { + // Call the function under test + success, err := database.UpdateVerificationStatusTo(ctx, models.New(nil, nil), email, true) + + // Validate the result + assert.Error(t, err) + assert.False(t, success) + }) + mt.Run("Update verification status successfully", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateSuccessResponse()) @@ -613,6 +708,15 @@ func TestCheckIfUserIsAdmin(t *testing.T) { email := "test@example.com" + mt.Run("Nil database", func(mt *mtest.T) { + // Call the function under test + isAdmin, err := database.CheckIfUserIsAdmin(ctx, models.New(nil, nil), email) + + // Validate the result + assert.Error(t, err) + assert.False(t, isAdmin) + }) + mt.Run("User is admin", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateCursorResponse(1, "Occupi.Users", mtest.FirstBatch, bson.D{ {Key: "email", Value: email}, @@ -658,308 +762,190 @@ func TestCheckIfUserIsAdmin(t *testing.T) { // Test AddResetToken func TestAddResetToken(t *testing.T) { - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - mt.Run("success", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateSuccessResponse()) + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) + + mt.Run("success", func(mt *mtest.T) { + mt.AddMockResponses(mtest.CreateSuccessResponse()) - email := "test@example.com" - resetToken := "token123" - expirationTime := time.Now().Add(1 * time.Hour) + email := "test@example.com" + resetToken := "token123" + expirationTime := time.Now().Add(1 * time.Hour) - success, err := database.AddResetToken(context.Background(), mt.Client, email, resetToken, expirationTime) + success, err := database.AddResetToken(context.Background(), mt.Client, email, resetToken, expirationTime) - assert.NoError(t, err) - assert.True(t, success) - }) + assert.NoError(t, err) + assert.True(t, success) + }) - mt.Run("error", func(mt *mtest.T) { - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 11000, - Message: "duplicate key error", - })) + mt.Run("error", func(mt *mtest.T) { + mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ + Code: 11000, + Message: "duplicate key error", + })) - email := "test@example.com" - resetToken := "token123" - expirationTime := time.Now().Add(1 * time.Hour) + email := "test@example.com" + resetToken := "token123" + expirationTime := time.Now().Add(1 * time.Hour) - success, err := database.AddResetToken(context.Background(), mt.Client, email, resetToken, expirationTime) + success, err := database.AddResetToken(context.Background(), mt.Client, email, resetToken, expirationTime) - assert.Error(t, err) - assert.False(t, success) - }) + assert.Error(t, err) + assert.False(t, success) + }) } // Test GetEmailByResetToken func TestGetEmailByResetToken(t *testing.T) { - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - mt.Run("success", func(mt *mtest.T) { - expectedEmail := "test@example.com" - resetToken := "token123" + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - mt.AddMockResponses(mtest.CreateCursorResponse(1, "Occupi.ResetTokens", mtest.FirstBatch, bson.D{ - {Key: "email", Value: expectedEmail}, - {Key: "token", Value: resetToken}, - })) + mt.Run("success", func(mt *mtest.T) { + expectedEmail := "test@example.com" + resetToken := "token123" - email, err := database.GetEmailByResetToken(context.Background(), mt.Client, resetToken) + mt.AddMockResponses(mtest.CreateCursorResponse(1, "Occupi.ResetTokens", mtest.FirstBatch, bson.D{ + {Key: "email", Value: expectedEmail}, + {Key: "token", Value: resetToken}, + })) - assert.NoError(t, err) - assert.Equal(t, expectedEmail, email) - }) + email, err := database.GetEmailByResetToken(context.Background(), mt.Client, resetToken) - mt.Run("not found", func(mt *mtest.T) { - resetToken := "nonexistenttoken" + assert.NoError(t, err) + assert.Equal(t, expectedEmail, email) + }) - mt.AddMockResponses(mtest.CreateCursorResponse(0, "Occupi.ResetTokens", mtest.FirstBatch)) + mt.Run("not found", func(mt *mtest.T) { + resetToken := "nonexistenttoken" - email, err := database.GetEmailByResetToken(context.Background(), mt.Client, resetToken) + mt.AddMockResponses(mtest.CreateCursorResponse(0, "Occupi.ResetTokens", mtest.FirstBatch)) - assert.Error(t, err) - assert.Equal(t, "", email) - }) + email, err := database.GetEmailByResetToken(context.Background(), mt.Client, resetToken) + + assert.Error(t, err) + assert.Equal(t, "", email) + }) } // Test CheckResetToken func TestCheckResetToken(t *testing.T) { - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - gin.SetMode(gin.TestMode) - ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + gin.SetMode(gin.TestMode) + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) - mt.Run("valid token", func(mt *mtest.T) { - email := "test@example.com" - token := "validtoken" - expireWhen := time.Now().Add(1 * time.Hour) + mt.Run("valid token", func(mt *mtest.T) { + email := "test@example.com" + token := "validtoken" + expireWhen := time.Now().Add(1 * time.Hour) - mt.AddMockResponses(mtest.CreateCursorResponse(1, "Occupi.ResetTokens", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email}, - {Key: "token", Value: token}, - {Key: "expireWhen", Value: expireWhen}, - })) + mt.AddMockResponses(mtest.CreateCursorResponse(1, "Occupi.ResetTokens", mtest.FirstBatch, bson.D{ + {Key: "email", Value: email}, + {Key: "token", Value: token}, + {Key: "expireWhen", Value: expireWhen}, + })) - valid, err := database.CheckResetToken(ctx, mt.Client, email, token) + valid, err := database.CheckResetToken(ctx, mt.Client, email, token) - assert.NoError(t, err) - assert.True(t, valid) - }) + assert.NoError(t, err) + assert.True(t, valid) + }) - mt.Run("expired token", func(mt *mtest.T) { - email := "test@example.com" - token := "expiredtoken" - expireWhen := time.Now().Add(-1 * time.Hour) + mt.Run("expired token", func(mt *mtest.T) { + email := "test@example.com" + token := "expiredtoken" + expireWhen := time.Now().Add(-1 * time.Hour) - mt.AddMockResponses(mtest.CreateCursorResponse(1, "Occupi.ResetTokens", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email}, - {Key: "token", Value: token}, - {Key: "expireWhen", Value: expireWhen}, - })) + mt.AddMockResponses(mtest.CreateCursorResponse(1, "Occupi.ResetTokens", mtest.FirstBatch, bson.D{ + {Key: "email", Value: email}, + {Key: "token", Value: token}, + {Key: "expireWhen", Value: expireWhen}, + })) - valid, err := database.CheckResetToken(ctx, mt.Client, email, token) + valid, err := database.CheckResetToken(ctx, mt.Client, email, token) - assert.NoError(t, err) - assert.False(t, valid) - }) + assert.NoError(t, err) + assert.False(t, valid) + }) - mt.Run("token not found", func(mt *mtest.T) { - email := "test@example.com" - token := "nonexistenttoken" + mt.Run("token not found", func(mt *mtest.T) { + email := "test@example.com" + token := "nonexistenttoken" - mt.AddMockResponses(mtest.CreateCursorResponse(0, "Occupi.ResetTokens", mtest.FirstBatch)) + mt.AddMockResponses(mtest.CreateCursorResponse(0, "Occupi.ResetTokens", mtest.FirstBatch)) - valid, err := database.CheckResetToken(ctx, mt.Client, email, token) + valid, err := database.CheckResetToken(ctx, mt.Client, email, token) - assert.Error(t, err) - assert.False(t, valid) - }) + assert.Error(t, err) + assert.False(t, valid) + }) } // Test UpdateUserPassword func TestUpdateUserPassword(t *testing.T) { - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - + mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - gin.SetMode(gin.TestMode) - ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + gin.SetMode(gin.TestMode) + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) - mt.Run("success", func(mt *mtest.T) { - email := "test@example.com" - newPassword := "newpassword123" + mt.Run("success", func(mt *mtest.T) { + email := "test@example.com" + newPassword := "newpassword123" - mt.AddMockResponses(mtest.CreateSuccessResponse()) + mt.AddMockResponses(mtest.CreateSuccessResponse()) - success, err := database.UpdateUserPassword(ctx, mt.Client, email, newPassword) + success, err := database.UpdateUserPassword(ctx, mt.Client, email, newPassword) - assert.NoError(t, err) - assert.True(t, success) - }) + assert.NoError(t, err) + assert.True(t, success) + }) - mt.Run("error", func(mt *mtest.T) { - email := "test@example.com" - newPassword := "newpassword123" + mt.Run("error", func(mt *mtest.T) { + email := "test@example.com" + newPassword := "newpassword123" - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 11000, - Message: "update error", - })) + mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ + Code: 11000, + Message: "update error", + })) - success, err := database.UpdateUserPassword(ctx, mt.Client, email, newPassword) + success, err := database.UpdateUserPassword(ctx, mt.Client, email, newPassword) - assert.Error(t, err) - assert.False(t, success) - }) + assert.Error(t, err) + assert.False(t, success) + }) } // Test ClearResetToken func TestClearResetToken(t *testing.T) { - mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - - - gin.SetMode(gin.TestMode) - ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) - - mt.Run("success", func(mt *mtest.T) { - email := "test@example.com" - token := "token123" - - mt.AddMockResponses(mtest.CreateSuccessResponse()) - - success, err := database.ClearResetToken(ctx, mt.Client, email, token) - - assert.NoError(t, err) - assert.True(t, success) - }) - - mt.Run("error", func(mt *mtest.T) { - email := "test@example.com" - token := "token123" - - mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ - Code: 11000, - Message: "delete error", - })) - - success, err := database.ClearResetToken(ctx, mt.Client, email, token) - - assert.Error(t, err) - assert.False(t, success) - }) -} -/* - -type MockUpdateVerificationStatus struct { - mock.Mock -} - -func (m *MockUpdateVerificationStatus) UpdateVerificationStatusTo(ctx *gin.Context, db *mongo.Client, email string, status bool) (bool, error) { - args := m.Called(ctx, db, email, status) - return args.Bool(0), args.Error(1) -} - -func TestCheckIfNextVerificationDateIsDue(t *testing.T) { - // Setup mock MongoDB instance mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - gin.SetMode(configs.GetGinRunMode()) - - // Create a new HTTP request with the POST method. - req, _ := http.NewRequest("POST", "/", nil) - - // Create a new ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - w := httptest.NewRecorder() + gin.SetMode(gin.TestMode) + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) - // Create a new context with the Request and ResponseWriter. - ctx, _ := gin.CreateTestContext(w) - ctx.Request = req + mt.Run("success", func(mt *mtest.T) { + email := "test@example.com" + token := "token123" - // Optionally, set any values in the context. - ctx.Set("test", "test") - - email := "test@example.com" - - mt.Run("Next verification date is due", func(mt *mtest.T) { - nextVerificationDate := time.Now().Add(-24 * time.Hour) - mt.AddMockResponses(mtest.CreateCursorResponse(1, "Occupi.Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email}, - {Key: "nextVerificationDate", Value: nextVerificationDate}, - })) - - mockUpdate := new(MockUpdateVerificationStatus) - mockUpdate.On("UpdateVerificationStatusTo", ctx, models.New(mt.Client, nil), email, false).Return(true, nil) - - // Replace the original function with the mock - originalFunc := database.UpdateVerificationStatusTo - database.UpdateVerificationStatusTo = mockUpdate.UpdateVerificationStatusTo - defer func() { database.UpdateVerificationStatusTo = originalFunc }() + mt.AddMockResponses(mtest.CreateSuccessResponse()) - // Call the function under test - due, err := database.CheckIfNextVerificationDateIsDue(ctx, models.New(mt.Client, nil), email) + success, err := database.ClearResetToken(ctx, mt.Client, email, token) - // Validate the result assert.NoError(t, err) - assert.True(t, due) - - // Verify the mock - mockUpdate.AssertCalled(t, "UpdateVerificationStatusTo", ctx, models.New(mt.Client, nil), email, false) + assert.True(t, success) }) - mt.Run("Next verification date is not due", func(mt *mtest.T) { - nextVerificationDate := time.Now().Add(24 * time.Hour) - mt.AddMockResponses(mtest.CreateCursorResponse(1, "Occupi.Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email}, - {Key: "nextVerificationDate", Value: nextVerificationDate}, - })) - - // Call the function under test - due, err := database.CheckIfNextVerificationDateIsDue(ctx, models.New(mt.Client, nil), email) + mt.Run("error", func(mt *mtest.T) { + email := "test@example.com" + token := "token123" - // Validate the result - assert.NoError(t, err) - assert.False(t, due) - }) - - mt.Run("FindOne error", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ Code: 11000, - Message: "find error", - })) - - // Call the function under test - due, err := database.CheckIfNextVerificationDateIsDue(ctx, models.New(mt.Client, nil), email) - - // Validate the result - assert.Error(t, err) - assert.False(t, due) - }) - - mt.Run("UpdateVerificationStatusTo error", func(mt *mtest.T) { - nextVerificationDate := time.Now().Add(-24 * time.Hour) - mt.AddMockResponses(mtest.CreateCursorResponse(1, "Occupi.Users", mtest.FirstBatch, bson.D{ - {Key: "email", Value: email}, - {Key: "nextVerificationDate", Value: nextVerificationDate}, + Message: "delete error", })) - mockUpdate := new(MockUpdateVerificationStatus) - mockUpdate.On("UpdateVerificationStatusTo", ctx, models.New(mt.Client, nil), email, false).Return(false, assert.AnError) - - // Replace the original function with the mock - originalFunc := database.UpdateVerificationStatusTo - database.UpdateVerificationStatusTo = mockUpdate.UpdateVerificationStatusTo - defer func() { database.UpdateVerificationStatusTo = originalFunc }() + success, err := database.ClearResetToken(ctx, mt.Client, email, token) - // Call the function under test - due, err := database.CheckIfNextVerificationDateIsDue(ctx, models.New(mt.Client, nil), email) - - // Validate the result assert.Error(t, err) - assert.False(t, due) - - // Verify the mock - mockUpdate.AssertCalled(t, "UpdateVerificationStatusTo", ctx, models.New(mt.Client, nil), email, false) + assert.False(t, success) }) } -*/ - diff --git a/occupi-backend/tests/main_test.go b/occupi-backend/tests/main_test.go index 1933429d..8dd1ab5e 100644 --- a/occupi-backend/tests/main_test.go +++ b/occupi-backend/tests/main_test.go @@ -27,6 +27,9 @@ func TestMain(m *testing.M) { // setup logger to log all server interactions utils.SetupLogger() + // clean the database + data.CleanDatabase() + // begin seeding the mock database data.SeedMockDatabase("../data/test_data.json")