From 355a0805292b2193054cc7d1f2660a3dc0e247f7 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 25 Jun 2024 23:18:50 +0200 Subject: [PATCH 01/54] chore: Add MongoDB service for testing and seed database before running tests --- .github/workflows/lint-test-build-golang.yml | 16 ++++- occupi-backend/occupi.bat | 12 +--- occupi-backend/occupi.sh | 10 +--- occupi-backend/pkg/database/seed.go | 61 ++++++++++++++++++++ occupi-backend/pkg/database/test_data.json | 3 + 5 files changed, 83 insertions(+), 19 deletions(-) create mode 100644 occupi-backend/pkg/database/seed.go create mode 100644 occupi-backend/pkg/database/test_data.json diff --git a/.github/workflows/lint-test-build-golang.yml b/.github/workflows/lint-test-build-golang.yml index ec411998..0ce47e07 100644 --- a/.github/workflows/lint-test-build-golang.yml +++ b/.github/workflows/lint-test-build-golang.yml @@ -44,6 +44,17 @@ jobs: name: Test runs-on: ubuntu-latest + services: + mongo: + image: mongo:latest + ports: + - 27017:27017 + options: >- + --health-cmd "mongosh --eval 'db.adminCommand({ping: 1})'" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - name: Checkout code uses: actions/checkout@v4 @@ -55,7 +66,7 @@ jobs: - name: Decrypt env variables run: | - echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 .env.gpg > .env + echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 .test.env.gpg > .test.env - name: Decrypt key file run: | @@ -65,6 +76,9 @@ jobs: run: | echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 cert.pem.gpg > cert.pem + - name: Seed MongoDB + run: go run pkg/database/seed.go + - name: Run tests run: | go test -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 diff --git a/occupi-backend/occupi.bat b/occupi-backend/occupi.bat index 5d74bdb6..5fbf509e 100644 --- a/occupi-backend/occupi.bat +++ b/occupi-backend/occupi.bat @@ -13,17 +13,11 @@ if "%1 %2" == "run dev" ( ) else if "%1 %2" == "build prod" ( go build cmd/occupi-backend/main.go exit /b 0 -) else if "%1 %2" == "docker build" ( - docker-compose build - exit /b 0 -) else if "%1 %2" == "docker up" ( - docker-compose up - exit /b 0 ) else if "%1" == "test" ( - go test -v ./tests/... + go run pkg/database/seed.go && go test -v ./tests/... exit /b 0 ) else if "%1 %2" == "test codecov" ( - go test -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 + go run pkg/database/seed.go && go test -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 exit /b 0 ) else if "%1" == "lint" ( golangci-lint run @@ -45,8 +39,6 @@ echo run dev : go run -v cmd/occupi-backend/main.go echo run prod : go run cmd/occupi-backend/main.go echo build dev : go build -v cmd/occupi-backend/main.go echo build prod : go build cmd/occupi-backend/main.go -echo docker build : docker-compose build -echo docker up : docker-compose up echo test : go test ./tests/... echo test codecov : go test ./tests/... -race -coverprofile=coverage.out -covermode=atomic echo lint : golangci-lint run diff --git a/occupi-backend/occupi.sh b/occupi-backend/occupi.sh index 684a5d6e..ad3b0e75 100644 --- a/occupi-backend/occupi.sh +++ b/occupi-backend/occupi.sh @@ -8,8 +8,6 @@ print_help() { echo " run prod -> go run cmd/occupi-backend/main.go" echo " build dev -> go build -v cmd/occupi-backend/main.go" echo " build prod -> go build cmd/occupi-backend/main.go" - echo " docker build -> docker-compose build" - echo " docker up -> docker-compose up" echo " test -> go test ./tests/..." echo " test codecov -> go test ./tests/... -race -coverprofile=coverage.out -covermode=atomic" echo " lint -> golangci-lint run" @@ -24,14 +22,10 @@ elif [ "$1" = "build" ] && [ "$2" = "dev" ]; then go build -v cmd/occupi-backend/main.go elif [ "$1" = "build" ] && [ "$2" = "prod" ]; then go build cmd/occupi-backend/main.go -elif [ "$1" = "docker" ] && [ "$2" = "build" ]; then - docker-compose build -elif [ "$1" = "docker" ] && [ "$2" = "up" ]; then - docker-compose up elif [ "$1" = "test" ]; then - go test -v ./tests/... + go run pkg/database/seed.go && go test -v ./tests/... elif [ "$1" = "test" ] && [ "$2" = "codecov" ]; then - go test -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 + go run pkg/database/seed.go && go test -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 elif [ "$1" = "lint" ]; then golangci-lint run elif [ "$1" = "help" ] || [ -z "$1" ]; then diff --git a/occupi-backend/pkg/database/seed.go b/occupi-backend/pkg/database/seed.go new file mode 100644 index 00000000..7e11b7a9 --- /dev/null +++ b/occupi-backend/pkg/database/seed.go @@ -0,0 +1,61 @@ +package database + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + + "github.com/joho/godotenv" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + + "github.com/COS301-SE-2024/occupi/occupi-backend/configs" + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils" +) + +type Item struct { + Name string `json:"name"` + Value string `json:"value"` +} + +func main() { + // Load environment variables from .env file + if err := godotenv.Load("../.env"); err != nil { + log.Fatal("Error loading .env file: ", err) + } + + // setup logger to log all server interactions + utils.SetupLogger() + + // connect to the database + db := ConnectToDatabase() + collectionName := "your_collection" + + collection := db.Database(configs.GetMongoDBName()).Collection(collectionName) + + data, err := ioutil.ReadFile("path/to/your/test_data.json") + if err != nil { + log.Fatalf("Failed to read JSON file: %v", err) + } + + fmt.Println("Successfully seeded test data into MongoDB") +} + +// Function to insert data if the collection is empty +func insertData(collection *mongo.Collection, documents []interface{}) { + count, err := collection.CountDocuments(context.Background(), bson.D{}) + if err != nil { + log.Fatalf("Failed to count documents: %v", err) + } + if count == 0 { + _, err := collection.InsertMany(context.Background(), documents) + if err != nil { + log.Fatalf("Failed to insert documents into %s collection: %v", collection.Name(), err) + } + fmt.Printf("Successfully seeded data into %s collection\n", collection.Name()) + } else { + fmt.Printf("Collection %s already has %d documents, skipping seeding\n", collection.Name(), count) + } +} diff --git a/occupi-backend/pkg/database/test_data.json b/occupi-backend/pkg/database/test_data.json new file mode 100644 index 00000000..544b7b4d --- /dev/null +++ b/occupi-backend/pkg/database/test_data.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file From bf3708e5a7b350cc0647d84e9a350f81423968b6 Mon Sep 17 00:00:00 2001 From: Kamogelo Moeketse Date: Wed, 26 Jun 2024 05:28:55 +0200 Subject: [PATCH 02/54] Android compatibility done --- frontend/occupi-mobile4/package-lock.json | 19 +++ frontend/occupi-mobile4/package.json | 1 + .../screens/Booking/BookRoom.tsx | 157 +++++++++--------- .../screens/Booking/ViewBookingDetails.tsx | 8 +- .../screens/Booking/ViewBookings.tsx | 63 +++---- .../screens/Dashboard/Dashboard.tsx | 20 +-- .../screens/Login/OtpVerification.tsx | 20 +-- .../occupi-mobile4/screens/Login/SignIn.tsx | 14 +- .../occupi-mobile4/screens/Login/SignUp.tsx | 14 +- .../screens/Office/BookingDetails.tsx | 29 ++-- .../screens/Office/OfficeDetails.tsx | 23 ++- .../screens/Profile/Profile.tsx | 8 +- .../screens/Profile/Settings.tsx | 10 +- 13 files changed, 206 insertions(+), 180 deletions(-) diff --git a/frontend/occupi-mobile4/package-lock.json b/frontend/occupi-mobile4/package-lock.json index 04a45e25..a40cd5e0 100644 --- a/frontend/occupi-mobile4/package-lock.json +++ b/frontend/occupi-mobile4/package-lock.json @@ -55,6 +55,7 @@ "react-native-responsive-screen": "^1.4.2", "react-native-safe-area-context": "^4.10.1", "react-native-screens": "3.31.1", + "react-native-skeleton-content": "^1.0.13", "react-native-svg": "^15.3.0", "react-native-web": "~0.19.10", "zod": "^3.23.8" @@ -19650,6 +19651,24 @@ "react-native": "*" } }, + "node_modules/react-native-skeleton-content": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/react-native-skeleton-content/-/react-native-skeleton-content-1.0.13.tgz", + "integrity": "sha512-WgwDnBEdgFNh63BkscV3FRvDKHbIFYxu0MiJkuySHYKIiexYu75tnNBe8ONkM0hQEJ9pO6pg5+c8uuUmY/uoXA==", + "dependencies": { + "expo-linear-gradient": "^8.0.0", + "lodash": "^4.17.14" + } + }, + "node_modules/react-native-skeleton-content/node_modules/expo-linear-gradient": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-8.4.0.tgz", + "integrity": "sha512-f9JOXaIl0MR8RBYRIud5UAsEi52oz81XhQs73VUpujemHjOyHmrZa6dqwf399YOwI/WBwbpcINcUIw/mCYE1mA==", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-svg": { "version": "15.3.0", "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.3.0.tgz", diff --git a/frontend/occupi-mobile4/package.json b/frontend/occupi-mobile4/package.json index 277e1696..9a6ca019 100644 --- a/frontend/occupi-mobile4/package.json +++ b/frontend/occupi-mobile4/package.json @@ -62,6 +62,7 @@ "react-native-responsive-screen": "^1.4.2", "react-native-safe-area-context": "^4.10.1", "react-native-screens": "3.31.1", + "react-native-skeleton-content": "^1.0.13", "react-native-svg": "^15.3.0", "react-native-web": "~0.19.10", "zod": "^3.23.8" diff --git a/frontend/occupi-mobile4/screens/Booking/BookRoom.tsx b/frontend/occupi-mobile4/screens/Booking/BookRoom.tsx index 4d11382f..4deef071 100644 --- a/frontend/occupi-mobile4/screens/Booking/BookRoom.tsx +++ b/frontend/occupi-mobile4/screens/Booking/BookRoom.tsx @@ -102,90 +102,93 @@ const BookRoom = () => { const roomPairs = groupDataInPairs(roomData); return ( - - - - Book - - - Quick search for an office - - - Rooms - - {layout === "row" ? ( - - - - ) : ( - - - - )} - + <> + + + + Book + + + Quick search for an office + + + Rooms + + {layout === "row" ? ( + + + + ) : ( + + + + )} + + - - {layout === "grid" ? ( - - {roomPairs.map((pair, index) => ( - - {pair.map((room, idx) => ( - router.push({ pathname: '/office-details', params: { roomData: JSON.stringify(room) } })}> - - - {room.roomName} - - - {room.description.length > 20 ? `${room.description.substring(0, 40)}...` : room.description} - - Floor: {room.floorNo === 0 ? 'G' : room.floorNo} - - - {room.minOccupancy} - {room.maxOccupancy} + {layout === "grid" ? ( + + {roomPairs.map((pair, index) => ( + + {pair.map((room, idx) => ( + router.push({ pathname: '/office-details', params: { roomData: JSON.stringify(room) } })}> + + + {room.roomName} + + + {room.description.length > 20 ? `${room.description.substring(0, 40)}...` : room.description} + + Floor: {room.floorNo === 0 ? 'G' : room.floorNo} + + + {room.minOccupancy} - {room.maxOccupancy} + + + + + Available: now + + - - - Available: now - - + + ))} + + ))} + + ) : ( + + {roomData.map((room, idx) => ( + router.push({ pathname: '/office-details', params: { roomData: JSON.stringify(room) } })}> + + + {room.roomName} + + + {room.description.length > 20 ? `${room.description.substring(0, 68)}...` : room.description} + + Floor: {room.floorNo === 0 ? "G" : room.floorNo} + + + {room.minOccupancy} - {room.maxOccupancy} - - ))} - - ))} - - ) : ( - - {roomData.map((room, idx) => ( - router.push({ pathname: '/office-details', params: { roomData: JSON.stringify(room) } })}> - - - {room.roomName} - - - {room.description.length > 20 ? `${room.description.substring(0, 68)}...` : room.description} - - Floor: {room.floorNo === 0 ? "G" : room.floorNo} - - - {room.minOccupancy} - {room.maxOccupancy} + + + Available: now + + - - - Available: now - - - - - - ))} - - )} - - + + ))} + + )} + + + + ); }; diff --git a/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx b/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx index e013321d..5b489eed 100644 --- a/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx +++ b/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx @@ -64,7 +64,7 @@ const ViewBookingDetails = (bookingId, roomName) => { placement: 'top', render: ({ id }) => { return ( - + {data.message} ); @@ -76,7 +76,7 @@ const ViewBookingDetails = (bookingId, roomName) => { placement: 'top', render: ({ id }) => { return ( - + {data.message} ); @@ -113,7 +113,7 @@ const ViewBookingDetails = (bookingId, roomName) => { placement: 'top', render: ({ id }) => { return ( - + {data.message} ); @@ -126,7 +126,7 @@ const ViewBookingDetails = (bookingId, roomName) => { placement: 'top', render: ({ id }) => { return ( - + {data.message} ); diff --git a/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx b/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx index bc3a8a4c..f10b7d17 100644 --- a/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx +++ b/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx @@ -30,6 +30,7 @@ interface Room { minOccupancy: number; maxOccupancy: number; description: string; + emails: string[]; } const getTimeForSlot = (slot) => { @@ -180,12 +181,12 @@ const ViewBookings = () => { const roomPairs = groupDataInPairs(roomData); return ( - + - My bookings + My bookings - + { color={textColor} /> - - - Sort by: - + + + Sort by: + setSelectedSort(value)} items={[ @@ -205,10 +206,9 @@ const ViewBookings = () => { { label: 'Newest', value: 'Newest' }, ]} placeholder={{ label: 'Latest', value: null }} - backgroundColor={cardBackgroundColor} + // backgroundColor={cardBackgroundColor} style={{ inputIOS: { - placeholder: "Latest", fontSize: 16, paddingVertical: 8, borderWidth: 1, @@ -218,26 +218,29 @@ const ViewBookings = () => { color: textColor }, inputAndroid: { + alignItems: "center", + width: 130, + height: 60, + fontSize: 10, + // paddingVertical: 4, borderWidth: 1, borderRadius: 10, borderColor: cardBackgroundColor, - paddingRight: 30, // to ensure the text is never behind the icon + padding: 0, // to ensure the text is never behind the icon color: textColor }, }} - Icon={() => { - return ; - }} + /> {layout === "row" ? ( - + ) : ( - + )} @@ -248,7 +251,7 @@ const ViewBookings = () => { {roomPairs.map((pair, index) => ( { w="$full" h="$24" alt="image" - borderRadius="$10" + borderRadius={10} source={'https://content-files.shure.com/OriginFiles/BlogPosts/best-layouts-for-conference-rooms/img5.png'} /> { > {room.roomName} - - Attendees: {room.emails.length} + + Attendees: {room.emails.length} - Your booking time: + Your booking time: - + - {new Date().toDateString()} + {new Date().toDateString()} {slotToTime(room.slot)} @@ -318,10 +321,10 @@ const ViewBookings = () => { }}> image { }} > {room.roomName} - - Attendees: {room.emails.length} + + Attendees: {room.emails.length} - - Your booking time: - + + Your booking time: + - {new Date().toDateString()} + {new Date().toDateString()} {slotToTime(room.slot)} diff --git a/frontend/occupi-mobile4/screens/Dashboard/Dashboard.tsx b/frontend/occupi-mobile4/screens/Dashboard/Dashboard.tsx index 785f2c47..5a232518 100644 --- a/frontend/occupi-mobile4/screens/Dashboard/Dashboard.tsx +++ b/frontend/occupi-mobile4/screens/Dashboard/Dashboard.tsx @@ -51,7 +51,7 @@ const Dashboard = () => { toast.show({ placement: 'top', render: ({ id }) => ( - + Check in successful. Have a productive day! ), @@ -61,7 +61,7 @@ const Dashboard = () => { toast.show({ placement: 'top', render: ({ id }) => ( - + Travel safe. Have a lovely day further! ), @@ -74,7 +74,7 @@ const Dashboard = () => { const cardBackgroundColor = isDarkMode ? '#2C2C2E' : '#F3F3F3'; return ( - + @@ -92,23 +92,23 @@ const Dashboard = () => { style={{ width: wp('8%'), height: wp('8%'), flexDirection: 'column', tintColor: isDarkMode ? 'white' : 'black' }} /> - + - + {numbers[0]} - - {numbers[0]/10+5}% + + {numbers[0]/10+5}% - + {checkedIn ? ( - ) : ( - )} diff --git a/frontend/occupi-mobile4/screens/Login/OtpVerification.tsx b/frontend/occupi-mobile4/screens/Login/OtpVerification.tsx index 4c9dbe1a..6011eb01 100644 --- a/frontend/occupi-mobile4/screens/Login/OtpVerification.tsx +++ b/frontend/occupi-mobile4/screens/Login/OtpVerification.tsx @@ -58,12 +58,12 @@ const OTPVerification = () => { const onSubmit = async () => { console.log(email); const pin = otp.join(''); - const Count = otp.filter((value) => value !== '').length; - if (Count < 6) { - setValidationError('OTP must be at least 6 characters in length'); - return; - } - setValidationError(null); + // const Count = otp.filter((value) => value !== '').length; + // if (Count < 6) { + // setValidationError('OTP must be at least 6 characters in length'); + // return; + // } + // setValidationError(null); console.log(pin); setLoading(true); try { @@ -86,13 +86,13 @@ const OTPVerification = () => { placement: 'top', render: ({ id }) => { return ( - + {data.message} ); }, }); - router.push('/home'); + router.push('/login'); } else { setLoading(false); // console.log(data); @@ -100,7 +100,7 @@ const OTPVerification = () => { placement: 'top', render: ({ id }) => { return ( - + {data.message} ); @@ -211,7 +211,7 @@ const MainText = (email : string) => { fontSize={wp('5%')} fontWeight="$light" > - {' '+email} + {' '+String(email)} diff --git a/frontend/occupi-mobile4/screens/Login/SignIn.tsx b/frontend/occupi-mobile4/screens/Login/SignIn.tsx index eeab7397..4422e20f 100644 --- a/frontend/occupi-mobile4/screens/Login/SignIn.tsx +++ b/frontend/occupi-mobile4/screens/Login/SignIn.tsx @@ -114,7 +114,7 @@ const SignInForm = () => { placement: 'top', render: ({ id }) => { return ( - + Biometric authentication failed {result.error && {result.error.message}} @@ -128,7 +128,7 @@ const SignInForm = () => { placement: 'top', render: ({ id }) => { return ( - + Biometric authentication error {error.message} @@ -142,7 +142,7 @@ const SignInForm = () => { placement: 'top', render: ({ id }) => { return ( - + Biometric authentication not available ); @@ -180,7 +180,7 @@ const SignInForm = () => { placement: 'top', render: ({ id }) => { return ( - + {data.message} ); @@ -194,7 +194,7 @@ const SignInForm = () => { placement: 'top', render: ({ id }) => { return ( - + {data.message} ); @@ -283,7 +283,7 @@ const SignInForm = () => { }, }} render={({ field: { onChange, onBlur, value } }) => ( - + { }, }} render={({ field: { onChange, onBlur, value } }) => ( - + { placement: 'top', render: ({ id }) => { return ( - + {data.message} ); @@ -120,7 +120,7 @@ const SignUpForm = () => { placement: 'top', render: ({ id }) => { return ( - + {data.error.message} ); @@ -136,7 +136,7 @@ const SignUpForm = () => { placement: 'bottom right', render: ({ id }) => { return ( - + Passwords do not match ); @@ -217,7 +217,7 @@ const SignUpForm = () => { }, }} render={({ field: { onChange, onBlur, value } }) => ( - + { }, }} render={({ field: { onChange, onBlur, value } }) => ( - + { }, }} render={({ field: { onChange, onBlur, value } }) => ( - + { }, }} render={({ field: { onChange, onBlur, value } }) => ( - + { placement: 'top', render: ({ id }) => { return ( - + {data.message} ); @@ -163,7 +163,7 @@ const BookingDetails = () => { placement: 'top', render: ({ id }) => { return ( - + {data.message} ); @@ -295,6 +295,7 @@ const BookingDetails = () => { style={{ flex: 1, backgroundColor: isDark ? "#000" : "#fff", + paddingTop: 50 }} > { }} > navigation.goBack()}> - router.back()} /> + router.back()} /> {/* */} { {currentStep === 1 && ( - + { > */} - HDMI Room + HDMI Room Fast OLED {roomData.minOccupancy} - {roomData.maxOccupancy} Floor {roomData.floorNo === 0 ? 'G' : roomData.floorNo} - - + + Check in: {startTime} - + Check out: {endTime} - - + + ---------------------------------------------- - + - + - Attendees: + Attendees: {attendees.map((email, idx) => ( @@ -612,7 +613,7 @@ const BookingDetails = () => { {/* */} router.push('/home')}> - + Home diff --git a/frontend/occupi-mobile4/screens/Office/OfficeDetails.tsx b/frontend/occupi-mobile4/screens/Office/OfficeDetails.tsx index cc7c231b..d9b34feb 100644 --- a/frontend/occupi-mobile4/screens/Office/OfficeDetails.tsx +++ b/frontend/occupi-mobile4/screens/Office/OfficeDetails.tsx @@ -139,11 +139,10 @@ const OfficeDetails = () => { return ( <> {/* Top Section */} - - navigation.goBack()} /> - - {roomData2.roomName} - + + navigation.goBack()} /> + {roomData2.roomName} + @@ -165,7 +164,7 @@ const OfficeDetails = () => { slide3 */} - + { })} > {pages.map((page, index) => ( - + slide1 @@ -194,7 +193,7 @@ const OfficeDetails = () => { - {roomData2.roomName} + {roomData2.roomName} Fast OLED @@ -230,8 +229,8 @@ const OfficeDetails = () => { {/* Description */} - Description - + Description + {roomData2.description} @@ -280,7 +279,7 @@ const OfficeDetails = () => { /> {/* Check Availability Button */} - router.push({ pathname: '/booking-details', params: { email: userEmail, slot: slot, roomId: roomData2.roomId, floorNo: roomData2.floorNo, roomData: roomData } })}> + router.push({ pathname: '/booking-details', params: { email: userEmail, slot: slot, roomId: roomData2.roomId, floorNo: roomData2.floorNo, roomData: roomData } })}> { onConfirm={handleConfirm} onCancel={hideDatePicker} /> - Check availability + Check availability diff --git a/frontend/occupi-mobile4/screens/Profile/Profile.tsx b/frontend/occupi-mobile4/screens/Profile/Profile.tsx index 36f7ec75..2916da55 100644 --- a/frontend/occupi-mobile4/screens/Profile/Profile.tsx +++ b/frontend/occupi-mobile4/screens/Profile/Profile.tsx @@ -86,7 +86,7 @@ const Profile = () => { router.push('/settings')} /> @@ -131,7 +131,7 @@ const Profile = () => { { { { placement: 'top', render: ({ id }) => { return ( - + {data.message} ); @@ -63,7 +63,7 @@ const Settings = () => { placement: 'top', render: ({ id }) => { return ( - + {data.message} ); @@ -125,7 +125,7 @@ const Settings = () => { {item.description} - {item.accessoryRight ? item.accessoryRight() : } + {item.accessoryRight ? item.accessoryRight() : } ); @@ -142,7 +142,7 @@ const Settings = () => { - + {name} {/* handleNavigate('EditProfileScreen')} /> */} @@ -150,7 +150,7 @@ const Settings = () => { - + {data.map((item, index) => ( {renderListItem({ item })} From eaf8aa2d0aeb76e21a451562729b7b18aacc2a23 Mon Sep 17 00:00:00 2001 From: Kamogelo Moeketse Date: Wed, 26 Jun 2024 05:55:34 +0200 Subject: [PATCH 03/54] refresh functionality for my bookings --- .../screens/Booking/ViewBookings.tsx | 146 ++++++++++++++---- 1 file changed, 115 insertions(+), 31 deletions(-) diff --git a/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx b/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx index f10b7d17..7e28223a 100644 --- a/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx +++ b/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx @@ -1,5 +1,5 @@ -import { React, useEffect, useState } from 'react'; -import { ScrollView, useColorScheme, TouchableOpacity } from 'react-native'; +import React, { useEffect, useState, useCallback } from 'react'; +import { ScrollView, useColorScheme, TouchableOpacity, RefreshControl, StyleSheet } from 'react-native'; import { Icon, View, Text, Input, InputField, Image, Box, ChevronDownIcon, Toast, ToastTitle, @@ -83,7 +83,7 @@ const getTimeForSlot = (slot) => { return { startTime, endTime }; }; -const slotToTime = (slot : number) => { +const slotToTime = (slot: number) => { const { startTime, endTime } = getTimeForSlot(slot); return `${startTime} - ${endTime}` } @@ -97,7 +97,67 @@ const ViewBookings = () => { // const [selectedSort, setSelectedSort] = useState("newest"); // const [email, setEmail] = useState('kamogelomoeketse@gmail.com'); const router = useRouter(); - + const [refreshing, setRefreshing] = useState(false); + + + const onRefresh = React.useCallback(() => { + const fetchAllRooms = async () => { + console.log("heree"); + try { + const response = await fetch(`https://dev.occupi.tech/api/view-bookings?email=kamogelomoeketse@gmail.com`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const data = await response.json(); + if (response.ok) { + setRoomData(data.data || []); // Ensure data is an array + console.log(data); + // toast.show({ + // placement: 'top', + // render: ({ id }) => { + // return ( + // + // {data.message} + // + // ); + // }, + // }); + } else { + console.log(data); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + {data.error.message} + + ); + }, + }); + } + } catch (error) { + console.error('Error:', error); + toast.show({ + placement: 'top', + render: ({ id }) => { + return ( + + Network Error: {error.message} + + ); + }, + }); + } + }; + setRefreshing(true); + setTimeout(() => { + setRefreshing(false); + fetchAllRooms(); + }, 2000); + }, [toast]); + const toggleLayout = () => { setLayout((prevLayout) => (prevLayout === "row" ? "grid" : "row")); }; @@ -230,7 +290,7 @@ const ViewBookings = () => { color: textColor }, }} - + /> @@ -248,7 +308,13 @@ const ViewBookings = () => { {layout === "grid" ? ( - + + } + > {roomPairs.map((pair, index) => ( { }} > {pair.map((room) => ( - router.push({ pathname: '/viewbookingdetails', params: { roomData: JSON.stringify(room) } })} - style={{ - flex: 1, - borderWidth: 1, - borderColor: cardBackgroundColor, - borderRadius: 12, - backgroundColor: cardBackgroundColor, - marginHorizontal: 4, - width: '45%' - }}> + router.push({ pathname: '/viewbookingdetails', params: { roomData: JSON.stringify(room) } })} + style={{ + flex: 1, + borderWidth: 1, + borderColor: cardBackgroundColor, + borderRadius: 12, + backgroundColor: cardBackgroundColor, + marginHorizontal: 4, + width: '45%' + }}> { ))} ) : ( - + + } + > {roomData.map((room) => ( - router.push({ pathname: '/viewbookingdetails', params: { roomData: JSON.stringify(room) } })} - style={{ - flex: 1, - borderWidth: 1, - borderColor: cardBackgroundColor, - borderRadius: 12, - height: 160, - backgroundColor: cardBackgroundColor, - marginVertical: 4, - flexDirection: "row" - - }}> + router.push({ pathname: '/viewbookingdetails', params: { roomData: JSON.stringify(room) } })} + style={{ + flex: 1, + borderWidth: 1, + borderColor: cardBackgroundColor, + borderRadius: 12, + height: 160, + backgroundColor: cardBackgroundColor, + marginVertical: 4, + flexDirection: "row" + + }}> { ); }; +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + scrollView: { + flex: 1, + backgroundColor: 'pink', + alignItems: 'center', + justifyContent: 'center', + }, +}); + export default ViewBookings; From 0b304365f83c04b327610f197b5686956b1beebb Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Wed, 26 Jun 2024 12:06:17 +0200 Subject: [PATCH 04/54] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d7271613..b534f272 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,9 @@ ![Codecov](https://img.shields.io/codecov/c/github/COS301-SE-2024/occupi?style=flat-square) ![GitHub License](https://img.shields.io/github/license/COS301-SE-2024/occupi?style=flat-square) ![GitHub repo size](https://img.shields.io/github/repo-size/COS301-SE-2024/occupi?style=flat-square) -![Docs Website](https://img.shields.io/website?url=https%3A%2F%2Fdocs.occupi.tech&style=flat-square) +![Uptime Robot ratio (7 days)](https://img.shields.io/uptimerobot/ratio/7/m797164574-0d7a208e3fca09e737a9619f?style=flat-square) +![Uptime Robot ratio (7 days)](https://img.shields.io/uptimerobot/ratio/7/m797164576-1e9cec32af1ac1e298136819?style=flat-square) +![Uptime Robot ratio (7 days)](https://img.shields.io/uptimerobot/ratio/7/m797164567-64429e97f6d650162e9354dd?style=flat-square) ![GitHub Repo stars](https://img.shields.io/github/stars/COS301-SE-2024/occupi?style=flat-square) [![codecov](https://codecov.io/gh/COS301-SE-2024/occupi/graph/badge.svg?token=71QPCD9NNP)](https://codecov.io/gh/COS301-SE-2024/occupi) ![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/COS301-SE-2024/occupi?style=flat-square) From 6414a23f305841187bc8e2db595c5186be98eef1 Mon Sep 17 00:00:00 2001 From: Michael-u21546551 Date: Wed, 26 Jun 2024 12:28:08 +0200 Subject: [PATCH 05/54] Update index.mdx --- documentation/occupi-docs/pages/index.mdx | 120 ++++++++++++++++++++-- 1 file changed, 114 insertions(+), 6 deletions(-) diff --git a/documentation/occupi-docs/pages/index.mdx b/documentation/occupi-docs/pages/index.mdx index 152aeef5..aaf91d57 100644 --- a/documentation/occupi-docs/pages/index.mdx +++ b/documentation/occupi-docs/pages/index.mdx @@ -21,21 +21,129 @@ To get started, please feel free to explore the top navbar or the sidebar and na ![GitHub repo size](https://img.shields.io/github/repo-size/COS301-SE-2024/occupi?style=flat-square) - ![Docs website](https://img.shields.io/website?url=https%3A%2F%2Fcos301-se-2024.github.io%2Foccupi%2F&style=flat-square) + ![Uptime Robot ratio (7 days)](https://img.shields.io/uptimerobot/ratio/7/m797164574-0d7a208e3fca09e737a9619f?style=flat-square) + + ![Uptime Robot ratio (7 days)](https://img.shields.io/uptimerobot/ratio/7/m797164576-1e9cec32af1ac1e298136819?style=flat-square) + + ![Uptime Robot ratio (7 days)](https://img.shields.io/uptimerobot/ratio/7/m797164567-64429e97f6d650162e9354dd?style=flat-square) ![GitHub Repo stars](https://img.shields.io/github/stars/COS301-SE-2024/occupi?style=flat-square) - ![Deploy Landing page](https://github.com/COS301-SE-2024/occupi/actions/workflows/deploy-landing-page.yml/badge.svg) + [![codecov](https://codecov.io/gh/COS301-SE-2024/occupi/graph/badge.svg?token=71QPCD9NNP)](https://codecov.io/gh/COS301-SE-2024/occupi) - ![Deploy Docs site to Pages](https://github.com/COS301-SE-2024/occupi/actions/workflows/deploy-docs.yml/badge.svg) + ![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/COS301-SE-2024/occupi?style=flat-square)
-## Additional info +
+
+ + ![Deploy Docs](https://github.com/COS301-SE-2024/occupi/actions/workflows/deploy-docs.yml/badge.svg) + + ![Deploy Landing page](https://github.com/COS301-SE-2024/occupi/actions/workflows/deploy-landing-page.yml/badge.svg) + + ![Lint Test Occupi-mobile](https://github.com/COS301-SE-2024/occupi/actions/workflows/lint-test-mobile.yml/badge.svg) -//stub + ![Lint Test and Build Golang](https://github.com/COS301-SE-2024/occupi/actions/workflows/lint-test-build-golang.yml/badge.svg) + + ![Deploy api](https://github.com/COS301-SE-2024/occupi/actions/workflows/deploy-golang-develop.yml/badge.svg) + + ![Lint Test and Build Occupi-Web](https://github.com/COS301-SE-2024/occupi/actions/workflows/lint-test-build-web.yml/badge.svg) + +
+ +
+ +# Meet the team behind occupi + + + + + + + + + + + + + + + + + + + + + +
+ + +

Kamo

+ Project Manager, Fullstack Engineer
+ Kamo is a skilled software developer with a strong focus on database engineering and architecture. He is proficient in both front-end and back-end development, making him a well-rounded contributor to any team. +

+ + + + + + +
+ + +

Carey

+ Frontend Engineer, UI Engineer, Business Analyst, Testing Engineer
+ Carey has a solid understanding of business principles, business analysis and systems analysis with a strong support for technology-centered strategies that promote interactive learning and easier access to resources for independent study. +

+ + + + + + +
+ + +

Michael

+ Infrastructure management, Designer, Backend Engineer, DevOps, Frontend Engineer
+ Michael is a computer science enthusiast with a keen interest in creating engaging and aesthetically pleasing CS-related projects. His strengths lie in UI design, FullStack development, DevOps, and Systems Design, making him a versatile contributor to any team. +

+ + + + + + +
+ + +

Tinashe

+ Frontend Engineer, UI engineer, Business Analyst, Testing Engineer
+ Tinashe is a BSc Computer Science Major and Multimedia Minor student with a keen interest in UI/UX design and Human Computer Interaction. She loves learning and tackling new challenges. Her resilience makes her an asset in any team. +

+ + + + + + +
+ + +

Reta

+ Fullstack Engineer
+ Rethakgetse has a comprehensive skill set that spans both frontend and backend development, making him a well-rounded software developer. He is driven by a passion for problem-solving and a desire to make a tangible impact. He embraces the limitless possibilities offered by technology and is committed to leveraging his skills to develop transformative applications that address pressing industry needs, enhance operational efficiency, and improve customer satisfaction. +

+ + + + + + +
## Sponsors and Stakeholders

@@ -64,4 +172,4 @@ To get started, please feel free to explore the top navbar or the sidebar and na
- \ No newline at end of file + From cb1623612f1cfcd1541e2db7ca5754f2e005c42b Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Wed, 26 Jun 2024 15:34:25 +0200 Subject: [PATCH 06/54] Email formatting fixed and related functions refactored --- occupi-backend/pkg/handlers/api_handlers.go | 36 ++--------- occupi-backend/pkg/mail/email_format.go | 45 ++++++++++++-- occupi-backend/pkg/mail/mail.go | 66 ++++++++++++++++++++- 3 files changed, 110 insertions(+), 37 deletions(-) diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index 1c4c9214..7e572636 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -58,19 +58,7 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected RoomID,Slot,Emails[],Creator,FloorNo ", nil)) return } - // // Initialize the validator - // validate := validator.New() - - // // Validate the booking struct - // if err := validate.Struct(booking); err != nil { - // validationErrors := err.(validator.ValidationErrors) - // errorDetails := make(gin.H) - // for _, validationError := range validationErrors { - // errorDetails[validationError.Field()] = validationError.Tag() - // } - // ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Validation failed", constants.InvalidRequestPayloadCode, "Validation errors occurred", errorDetails)) - // return - // } + // Generate a unique ID for the booking booking.ID = primitive.NewObjectID().Hex() booking.OccupiID = utils.GenerateBookingID() @@ -83,15 +71,8 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { return } - // Prepare the email content - subject := "Booking Confirmation - Occupi" - body := mail.FormatBookingEmailBodyForBooker(booking.ID, booking.RoomID, booking.Slot, booking.Emails) - - // Send the confirmation email concurrently to all recipients - emailErrors := mail.SendMultipleEmailsConcurrently(booking.Emails, subject, body) - - if len(emailErrors) > 0 { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send confirmation email", constants.InternalServerErrorCode, "Failed to send confirmation email", nil)) + if err := mail.SendBookingEmails(booking); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send booking email", constants.InternalServerErrorCode, "Failed to send booking email", nil)) return } @@ -139,15 +120,8 @@ func CancelBooking(ctx *gin.Context, appsession *models.AppSession) { return } - // Prepare the email content - subject := "Booking Cancelled - Occupi" - body := mail.FormatBookingEmailBody(booking.ID, booking.RoomID, booking.Slot) - - // Send the confirmation email concurrently to all recipients - emailErrors := mail.SendMultipleEmailsConcurrently(booking.Emails, subject, body) - - if len(emailErrors) > 0 { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send cancellation email", constants.InternalServerErrorCode, "Failed to send cancellation email", nil)) + if err := mail.SendCancellationEmails(booking); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "An error occured", constants.InternalServerErrorCode, "Failed to send booking email", nil)) return } diff --git a/occupi-backend/pkg/mail/email_format.go b/occupi-backend/pkg/mail/email_format.go index 9ea9dc1d..846abae0 100644 --- a/occupi-backend/pkg/mail/email_format.go +++ b/occupi-backend/pkg/mail/email_format.go @@ -21,7 +21,7 @@ func FormatBookingEmailBody(bookingID string, roomID string, slot int) string { } // formats booking email body to send person who booked -func FormatBookingEmailBodyForBooker(bookingID string, roomID string, slot int, attendees []string) string { +func FormatBookingEmailBodyForBooker(bookingID string, roomID string, slot int, attendees []string, email string) string { listOfAttendees := "
    " for _, email := range attendees { listOfAttendees += "
  • " + email + "
  • " @@ -44,6 +44,23 @@ func FormatBookingEmailBodyForBooker(bookingID string, roomID string, slot int, ` + appendFooter() } +// formats cancellation email body to send person who booked +func FormatCancellationEmailBodyForBooker(bookingID string, roomID string, slot int, email string) string { + + return appendHeader("Cancellation") + ` +
    +

    Dear booker,

    +

    + You have successfully cancelled your booked office space. Here are the booking details:

    + Booking ID: ` + bookingID + `
    + Room ID: ` + roomID + `
    + Slot: ` + strconv.Itoa(slot) + `

    + Thank you,
    + The Occupi Team
    +

    +
    ` + appendFooter() +} + // formats booking email body to send attendees func FormatBookingEmailBodyForAttendees(bookingID string, roomID string, slot int, email string) string { return appendHeader("Booking") + ` @@ -61,6 +78,23 @@ func FormatBookingEmailBodyForAttendees(bookingID string, roomID string, slot in ` + appendFooter() } +// formats cancellation email body to send attendees +func FormatCancellationEmailBodyForAttendees(bookingID string, roomID string, slot int, email string) string { + return appendHeader("Booking") + ` +
    +

    Dear attendees,

    +

    + ` + email + ` has cancelled the booked office space with the following details:

    + Booking ID: ` + bookingID + `
    + Room ID: ` + roomID + `
    + Slot: ` + strconv.Itoa(slot) + `

    + If you have any questions, feel free to contact us.

    + Thank you,
    + The Occupi Team
    +

    +
    ` + appendFooter() +} + // formats verification email body func FormatEmailVerificationBody(otp string, email string) string { return appendHeader("Registration") + ` @@ -115,10 +149,11 @@ func appendHeader(title string) string { } func appendFooter() string { - return ` + return ` + ` diff --git a/occupi-backend/pkg/mail/mail.go b/occupi-backend/pkg/mail/mail.go index b6bc67ac..f5192a4c 100644 --- a/occupi-backend/pkg/mail/mail.go +++ b/occupi-backend/pkg/mail/mail.go @@ -1,9 +1,11 @@ package mail import ( + "errors" "sync" "github.com/COS301-SE-2024/occupi/occupi-backend/configs" + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" "gopkg.in/gomail.v2" ) @@ -29,7 +31,7 @@ func SendMail(to string, subject string, body string) error { return nil } -func SendMultipleEmailsConcurrently(emails []string, subject, body string) []string { +func SendMultipleEmailsConcurrently(emails []string, subject, body string, creator string) []string { // Use a WaitGroup to wait for all goroutines to complete var wg sync.WaitGroup var emailErrors []string @@ -52,3 +54,65 @@ func SendMultipleEmailsConcurrently(emails []string, subject, body string) []str return emailErrors } + +func SendBookingEmails(booking models.Booking) error { + // Prepare the email content + creatorSubject := "Booking Confirmation - Occupi" + creatorBody := FormatBookingEmailBodyForBooker(booking.ID, booking.RoomID, booking.Slot, booking.Emails, booking.Creator) + + // Prepare the email content for attendees + attendeesSubject := "You're invited to a Booking - Occupi" + attendeesBody := FormatBookingEmailBodyForAttendees(booking.ID, booking.RoomID, booking.Slot, booking.Creator) + + var attendees []string + for _, email := range booking.Emails { + if email != booking.Creator { + attendees = append(attendees, email) + } + } + + creatorEmailError := SendMail(booking.Creator, creatorSubject, creatorBody) + if creatorEmailError != nil { + return creatorEmailError + } + + // Send the confirmation email concurrently to all recipients + emailErrors := SendMultipleEmailsConcurrently(attendees, attendeesSubject, attendeesBody, booking.Creator) + + if len(emailErrors) > 0 { + return errors.New("failed to send booking emails") + } + + return nil +} + +func SendCancellationEmails(booking models.Booking) error { + // Prepare the email content + creatorSubject := "Booking Cancelled - Occupi" + creatorBody := FormatCancellationEmailBodyForBooker(booking.ID, booking.RoomID, booking.Slot, booking.Creator) + + // Prepare the email content for attendees + attendeesSubject := "Booking Cancelled - Occupi" + attendeesBody := FormatCancellationEmailBodyForAttendees(booking.ID, booking.RoomID, booking.Slot, booking.Creator) + + var attendees []string + for _, email := range booking.Emails { + if email != booking.Creator { + attendees = append(attendees, email) + } + } + + creatorEmailError := SendMail(booking.Creator, creatorSubject, creatorBody) + if creatorEmailError != nil { + return creatorEmailError + } + + // Send the confirmation email concurrently to all recipients + emailErrors := SendMultipleEmailsConcurrently(attendees, attendeesSubject, attendeesBody, booking.Creator) + + if len(emailErrors) > 0 { + return errors.New("failed to send booking emails") + } + + return nil +} From b020a704dd5f4cd38c3d877b470cd7ba44c06223 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 26 Jun 2024 18:50:30 +0200 Subject: [PATCH 07/54] chore: Added logic for testing with locally ready database instead of production database --- .github/workflows/deploy-golang-develop.yml | 57 -------- .github/workflows/deploy-golang-prod.yml | 57 -------- .github/workflows/lint-test-build-golang.yml | 34 +++-- occupi-backend/.test.env.gpg | Bin 0 -> 584 bytes occupi-backend/data/test_data.json | 1 + occupi-backend/occupi.bat | 4 +- occupi-backend/occupi.sh | 4 +- occupi-backend/pkg/constants/constants.go | 2 + occupi-backend/pkg/database/database.go | 13 +- occupi-backend/pkg/database/seed.go | 73 ++++++---- occupi-backend/pkg/database/test_data.json | 3 - occupi-backend/tests/authenticator_test.go | 21 --- occupi-backend/tests/handlers_test.go | 144 ++++++++----------- occupi-backend/tests/main_test.go | 31 ++++ occupi-backend/tests/middleware_test.go | 111 +++----------- presentation/Occupi/occupi-gradient.png | Bin 0 -> 15993 bytes 16 files changed, 198 insertions(+), 357 deletions(-) create mode 100644 occupi-backend/.test.env.gpg create mode 100644 occupi-backend/data/test_data.json delete mode 100644 occupi-backend/pkg/database/test_data.json create mode 100644 occupi-backend/tests/main_test.go create mode 100644 presentation/Occupi/occupi-gradient.png diff --git a/.github/workflows/deploy-golang-develop.yml b/.github/workflows/deploy-golang-develop.yml index d9d628b7..9d555db1 100644 --- a/.github/workflows/deploy-golang-develop.yml +++ b/.github/workflows/deploy-golang-develop.yml @@ -20,64 +20,7 @@ defaults: working-directory: occupi-backend jobs: - lint: - name: Lint - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.21' # Specify the Go version you are using - - - name: Install golangci-lint - run: | - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - - - name: Run golangci-lint - run: | - golangci-lint run - - test: - needs: lint - name: Test - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.21' # Specify the Go version you are using - - - name: Decrypt env variables - run: | - echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 .env.gpg > .env - - - name: Decrypt key file - run: | - echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 key.pem.gpg > key.pem - - - name: Decrypt cert file - run: | - echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 cert.pem.gpg > cert.pem - - - name: Run tests - run: | - go test -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 - - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v4.0.1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - build-test: - needs: test name: Build runs-on: ubuntu-latest diff --git a/.github/workflows/deploy-golang-prod.yml b/.github/workflows/deploy-golang-prod.yml index 9cfa7e78..325bfdab 100644 --- a/.github/workflows/deploy-golang-prod.yml +++ b/.github/workflows/deploy-golang-prod.yml @@ -12,64 +12,7 @@ defaults: working-directory: occupi-backend jobs: - lint: - name: Lint - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.21' # Specify the Go version you are using - - - name: Install golangci-lint - run: | - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - - - name: Run golangci-lint - run: | - golangci-lint run - - test: - needs: lint - name: Test - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.21' # Specify the Go version you are using - - - name: Decrypt env variables - run: | - echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 .env.gpg > .env - - - name: Decrypt key file - run: | - echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 key.pem.gpg > key.pem - - - name: Decrypt cert file - run: | - echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 cert.pem.gpg > cert.pem - - - name: Run tests - run: | - go test -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 - - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v4.0.1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - build: - needs: test name: Build runs-on: ubuntu-latest diff --git a/.github/workflows/lint-test-build-golang.yml b/.github/workflows/lint-test-build-golang.yml index 0ce47e07..36304f45 100644 --- a/.github/workflows/lint-test-build-golang.yml +++ b/.github/workflows/lint-test-build-golang.yml @@ -59,6 +59,29 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Wait for MongoDB to be ready + run: | + for i in {1..30}; do + if mongosh --eval 'db.adminCommand({ping: 1})' ${{ secrets.MONGO_DB_TEST_URL }}; then + echo "MongoDB is up" + break + fi + echo "Waiting for MongoDB to be ready..." + sleep 2 + done + + - name: Create MongoDB User + run: | + mongosh ${{ secrets.MONGO_DB_TEST_URL }}/admin --eval ' + db.createUser({ + user: ${{ secrets.MONGO_DB_TEST_USERNAME }}, + pwd: ${{ secrets.MONGO_DB_TEST_PASSWORD }}, + roles: [ + { role: "readWrite", db: ${{ secrets.MONGO_DB_TEST_DB }} } + ] + }); + ' + - name: Set up Go uses: actions/setup-go@v5 with: @@ -68,17 +91,6 @@ jobs: run: | echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 .test.env.gpg > .test.env - - name: Decrypt key file - run: | - echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 key.pem.gpg > key.pem - - - name: Decrypt cert file - run: | - echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 cert.pem.gpg > cert.pem - - - name: Seed MongoDB - run: go run pkg/database/seed.go - - name: Run tests run: | go test -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 diff --git a/occupi-backend/.test.env.gpg b/occupi-backend/.test.env.gpg new file mode 100644 index 0000000000000000000000000000000000000000..c4d97e702ba989222df807aaa4f2c7897d09bfee GIT binary patch literal 584 zcmV-O0=NB)4Fm}T0L~z|qy3S3jWfm? zr1{*gpN1nS)OcKc@f`MwJj18`&bz$Axh>!uT4_07V|ohrIRy|%RQ?96hKO2ZSB=TuGX%*Cc& ziv_;}48JOd9nXO{z`U5cE{;FJ-+FP8axUr72E8=$sh7=7(HQ*>`{$d+)-` zr|n5&y*3s%TcxzWTKha>1ja*+&c0NG08x_N()J zvp??W-Jr>KKua$^yk5r6{rhxf4tQG%Lg6GA+9EzD;sM42XSiH4KXzg%5Cu5v1s8ye4lb+! literal 0 HcmV?d00001 diff --git a/occupi-backend/data/test_data.json b/occupi-backend/data/test_data.json new file mode 100644 index 00000000..5dde7ba9 --- /dev/null +++ b/occupi-backend/data/test_data.json @@ -0,0 +1 @@ +{"otps":[],"bookings":[],"rooms":[],"users":[{"_id":"ay3mxkedhu1sceq34gxyf6oe","occupiID":"1","password":"password1234","email":"abcd@gmail.com","role":"basic","onSite":true,"isVerified":true,"nextVerificationDate":"1999-12-31T22:00:00Z"},{"_id":"h5wg54k5k15jie6hmb3iicjl","occupiID":"2","password":"password1234","email":"efgh@gmail.com","role":"admin","onSite":true,"isVerified":true,"nextVerificationDate":"1999-12-31T22:00:00Z"}]} \ No newline at end of file diff --git a/occupi-backend/occupi.bat b/occupi-backend/occupi.bat index 5fbf509e..18c02689 100644 --- a/occupi-backend/occupi.bat +++ b/occupi-backend/occupi.bat @@ -14,10 +14,10 @@ if "%1 %2" == "run dev" ( go build cmd/occupi-backend/main.go exit /b 0 ) else if "%1" == "test" ( - go run pkg/database/seed.go && go test -v ./tests/... + go test -v ./tests/... exit /b 0 ) else if "%1 %2" == "test codecov" ( - go run pkg/database/seed.go && go test -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 + go test -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 exit /b 0 ) else if "%1" == "lint" ( golangci-lint run diff --git a/occupi-backend/occupi.sh b/occupi-backend/occupi.sh index ad3b0e75..2bacff9a 100644 --- a/occupi-backend/occupi.sh +++ b/occupi-backend/occupi.sh @@ -23,9 +23,9 @@ elif [ "$1" = "build" ] && [ "$2" = "dev" ]; then elif [ "$1" = "build" ] && [ "$2" = "prod" ]; then go build cmd/occupi-backend/main.go elif [ "$1" = "test" ]; then - go run pkg/database/seed.go && go test -v ./tests/... + go test -v ./tests/... elif [ "$1" = "test" ] && [ "$2" = "codecov" ]; then - go run pkg/database/seed.go && go test -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 + go test -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 elif [ "$1" = "lint" ]; then golangci-lint run elif [ "$1" = "help" ] || [ -z "$1" ]; then diff --git a/occupi-backend/pkg/constants/constants.go b/occupi-backend/pkg/constants/constants.go index 3f2d0a6a..599815a1 100644 --- a/occupi-backend/pkg/constants/constants.go +++ b/occupi-backend/pkg/constants/constants.go @@ -8,3 +8,5 @@ const InternalServerErrorCode = "INTERNAL_SERVER_ERROR" const UnAuthorizedCode = "UNAUTHORIZED" const Admin = "admin" const Basic = "basic" + +const AdminDBAccessOption = "authSource=admin" diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index 24b38db3..8ab7a37a 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -20,7 +20,7 @@ import ( ) // attempts to and establishes a connection with the remote mongodb database -func ConnectToDatabase() *mongo.Client { +func ConnectToDatabase(args ...string) *mongo.Client { // MongoDB connection parameters username := configs.GetMongoDBUsername() password := configs.GetMongoDBPassword() @@ -32,7 +32,12 @@ func ConnectToDatabase() *mongo.Client { escapedPassword := url.QueryEscape(password) // Construct the connection URI - uri := fmt.Sprintf("%s://%s:%s@%s/%s", mongoDBStartURI, username, escapedPassword, clusterURI, dbName) + var uri string + if len(args) > 0 { + uri = fmt.Sprintf("%s://%s:%s@%s/%s?%s", mongoDBStartURI, username, escapedPassword, clusterURI, dbName, args[0]) + } else { + uri = fmt.Sprintf("%s://%s:%s@%s/%s", mongoDBStartURI, username, escapedPassword, clusterURI, dbName) + } // Set client options clientOptions := options.Client().ApplyURI(uri) @@ -40,13 +45,13 @@ func ConnectToDatabase() *mongo.Client { // Connect to MongoDB client, err := mongo.Connect(context.TODO(), clientOptions) if err != nil { - logrus.Error(err) + logrus.Fatal(err) } // Check the connection err = client.Ping(context.TODO(), nil) if err != nil { - logrus.Error(err) + logrus.Fatal(err) } logrus.Info("Connected to MongoDB!") diff --git a/occupi-backend/pkg/database/seed.go b/occupi-backend/pkg/database/seed.go index 7e11b7a9..a43a3162 100644 --- a/occupi-backend/pkg/database/seed.go +++ b/occupi-backend/pkg/database/seed.go @@ -2,45 +2,66 @@ package database import ( "context" - "encoding/json" - "fmt" - "io/ioutil" "log" + "os" - "github.com/joho/godotenv" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "github.com/COS301-SE-2024/occupi/occupi-backend/configs" - "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils" + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" ) -type Item struct { - Name string `json:"name"` - Value string `json:"value"` +type MockDatabase struct { + OTPS []models.OTP `json:"otps"` + Bookings []models.Booking `json:"bookings"` + Rooms []models.Room `json:"rooms"` + Users []models.User `json:"users"` } -func main() { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - log.Fatal("Error loading .env file: ", err) +func SeedMockDatabase(mockdatafilepath string) { + // connect to the database + db := ConnectToDatabase(constants.AdminDBAccessOption) + + // Read the JSON file + data, err := os.ReadFile(mockdatafilepath) + if err != nil { + log.Fatalf("Failed to read JSON file: %v", err) } - // setup logger to log all server interactions - utils.SetupLogger() + // Parse the JSON file + var mockDatabase MockDatabase + if err := bson.UnmarshalExtJSON(data, true, &mockDatabase); err != nil { + log.Fatalf("Failed to unmarshal JSON data: %v", err) + } - // connect to the database - db := ConnectToDatabase() - collectionName := "your_collection" + // Insert data into each collection + var otpsDocuments []interface{} + for _, otps := range mockDatabase.OTPS { + otpsDocuments = append(otpsDocuments, otps) + } + insertData(db.Database(configs.GetMongoDBName()).Collection("OTPS"), otpsDocuments) - collection := db.Database(configs.GetMongoDBName()).Collection(collectionName) + var BookingsDocuments []interface{} + for _, roomBooking := range mockDatabase.Bookings { + BookingsDocuments = append(BookingsDocuments, roomBooking) + } + insertData(db.Database(configs.GetMongoDBName()).Collection("RoomBooking"), BookingsDocuments) - data, err := ioutil.ReadFile("path/to/your/test_data.json") - if err != nil { - log.Fatalf("Failed to read JSON file: %v", err) + var roomsDocuments []interface{} + for _, rooms := range mockDatabase.Rooms { + roomsDocuments = append(roomsDocuments, rooms) + } + insertData(db.Database(configs.GetMongoDBName()).Collection("Rooms"), roomsDocuments) + + var usersDocuments []interface{} + for _, users := range mockDatabase.Users { + usersDocuments = append(usersDocuments, users) } + insertData(db.Database(configs.GetMongoDBName()).Collection("Users"), usersDocuments) - fmt.Println("Successfully seeded test data into MongoDB") + log.Println("Successfully seeded test data into MongoDB") } // Function to insert data if the collection is empty @@ -49,13 +70,15 @@ func insertData(collection *mongo.Collection, documents []interface{}) { if err != nil { log.Fatalf("Failed to count documents: %v", err) } - if count == 0 { + if count == 0 && len(documents) > 0 { _, err := collection.InsertMany(context.Background(), documents) if err != nil { log.Fatalf("Failed to insert documents into %s collection: %v", collection.Name(), err) } - fmt.Printf("Successfully seeded data into %s collection\n", collection.Name()) + log.Printf("Successfully seeded data into %s collection\n", collection.Name()) + } else if len(documents) == 0 { + log.Printf("No documents to insert into %s skipping seeding\n", collection.Name()) } else { - fmt.Printf("Collection %s already has %d documents, skipping seeding\n", collection.Name(), count) + log.Printf("Collection %s already has %d documents, skipping seeding\n", collection.Name(), count) } } diff --git a/occupi-backend/pkg/database/test_data.json b/occupi-backend/pkg/database/test_data.json deleted file mode 100644 index 544b7b4d..00000000 --- a/occupi-backend/pkg/database/test_data.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} \ No newline at end of file diff --git a/occupi-backend/tests/authenticator_test.go b/occupi-backend/tests/authenticator_test.go index 0d97e0d7..1e2b1e4e 100644 --- a/occupi-backend/tests/authenticator_test.go +++ b/occupi-backend/tests/authenticator_test.go @@ -4,7 +4,6 @@ import ( "testing" "time" - "github.com/joho/godotenv" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" @@ -13,11 +12,6 @@ import ( ) func TestGenerateToken(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - email := "test1@example.com" role := constants.Admin tokenString, expirationTime, err := authenticator.GenerateToken(email, role) @@ -35,11 +29,6 @@ func TestGenerateToken(t *testing.T) { } func TestValidateToken(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - email := "test2@example.com" role := constants.Admin tokenString, _, err := authenticator.GenerateToken(email, role) @@ -56,11 +45,6 @@ func TestValidateToken(t *testing.T) { } func TestValidateTokenExpired(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - email := "test3@example.com" role := constants.Admin @@ -79,11 +63,6 @@ func TestValidateTokenExpired(t *testing.T) { } func TestInvalidToken(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - // Test with an invalid token invalidTokenString := "invalid_token" claims, err := authenticator.ValidateToken(invalidTokenString) diff --git a/occupi-backend/tests/handlers_test.go b/occupi-backend/tests/handlers_test.go index 3e272c5d..30dbb777 100644 --- a/occupi-backend/tests/handlers_test.go +++ b/occupi-backend/tests/handlers_test.go @@ -2,7 +2,6 @@ package tests import ( "encoding/json" - "fmt" "net/http" "net/http/httptest" "strings" @@ -10,7 +9,6 @@ import ( "testing" "time" - "github.com/joho/godotenv" "github.com/stretchr/testify/assert" "github.com/gin-gonic/gin" @@ -21,24 +19,15 @@ import ( "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/router" - "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils" // "github.com/stretchr/testify/mock" ) func TestViewBookingsHandler(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - - // setup logger to log all server interactions - utils.SetupLogger() - // connect to the database - db := database.ConnectToDatabase() + db := database.ConnectToDatabase(constants.AdminDBAccessOption) // set gin run mode - gin.SetMode("test") + gin.SetMode(configs.GetGinRunMode()) // Create a Gin router r := gin.Default() @@ -129,20 +118,14 @@ func TestViewBookingsHandler(t *testing.T) { }) } } -func TestBookRoom(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - - // setup logger to log all server interactions - utils.SetupLogger() +/* +func TestBookRoom(t *testing.T) { // connect to the database - db := database.ConnectToDatabase() + db := database.ConnectToDatabase(constants.AdminDBAccessOption) // set gin run mode - gin.SetMode("test") + gin.SetMode(configs.GetGinRunMode()) // Create a Gin router r := gin.Default() @@ -250,21 +233,14 @@ func TestBookRoom(t *testing.T) { }) } } +*/ func TestPingRoute(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal(fmt.Printf("Error loading .env file with error as %s", err)) - } - - // setup logger to log all server interactions - utils.SetupLogger() - // connect to the database - db := database.ConnectToDatabase() + db := database.ConnectToDatabase(constants.AdminDBAccessOption) // set gin run mode - gin.SetMode("test") + gin.SetMode(configs.GetGinRunMode()) // Create a Gin router ginRouter := gin.Default() @@ -306,19 +282,11 @@ func TestPingRoute(t *testing.T) { } func TestRateLimit(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal(fmt.Printf("Error loading .env file with error as %s", err)) - } - - // setup logger to log all server interactions - utils.SetupLogger() - // connect to the database - db := database.ConnectToDatabase() + db := database.ConnectToDatabase(constants.AdminDBAccessOption) // set gin run mode - gin.SetMode("test") + gin.SetMode(configs.GetGinRunMode()) // Create a Gin router ginRouter := gin.Default() @@ -365,19 +333,11 @@ func TestRateLimit(t *testing.T) { } func TestRateLimitWithMultipleIPs(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal(fmt.Printf("Error loading .env file with error as %s", err)) - } - - // setup logger to log all server interactions - utils.SetupLogger() - // connect to the database - db := database.ConnectToDatabase() + db := database.ConnectToDatabase(constants.AdminDBAccessOption) // set gin run mode - gin.SetMode("test") + gin.SetMode(configs.GetGinRunMode()) // Create a Gin router ginRouter := gin.Default() @@ -468,19 +428,11 @@ func TestRateLimitWithMultipleIPs(t *testing.T) { } func TestInvalidLogoutHandler(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - - // setup logger to log all server interactions - utils.SetupLogger() - // connect to the database - db := database.ConnectToDatabase() + db := database.ConnectToDatabase(constants.AdminDBAccessOption) // set gin run mode - gin.SetMode("test") + gin.SetMode(configs.GetGinRunMode()) // Create a Gin router ginRouter := gin.Default() @@ -507,19 +459,11 @@ func TestInvalidLogoutHandler(t *testing.T) { } func TestValidLogoutHandler(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - - // setup logger to log all server interactions - utils.SetupLogger() - // connect to the database - db := database.ConnectToDatabase() + db := database.ConnectToDatabase(constants.AdminDBAccessOption) // set gin run mode - gin.SetMode("test") + gin.SetMode(configs.GetGinRunMode()) // Create a Gin router ginRouter := gin.Default() @@ -575,19 +519,11 @@ func TestValidLogoutHandler(t *testing.T) { } func TestValidLogoutHandlerFromDomains(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - - // setup logger to log all server interactions - utils.SetupLogger() - // connect to the database - db := database.ConnectToDatabase() + db := database.ConnectToDatabase(constants.AdminDBAccessOption) // set gin run mode - gin.SetMode("test") + gin.SetMode(configs.GetGinRunMode()) // Create a Gin router ginRouter := gin.Default() @@ -662,3 +598,45 @@ func TestValidLogoutHandlerFromDomains(t *testing.T) { // Wait for all goroutines to finish wg.Wait() } + +func TestMockDatabase(t *testing.T) { + // connect to the database + db := database.ConnectToDatabase(constants.AdminDBAccessOption) + + // set gin run mode + gin.SetMode(configs.GetGinRunMode()) + + // Create a Gin router + r := gin.Default() + + // Register the route + router.OccupiRouter(r, db) + + token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/resource-auth", nil) + req.AddCookie(&http.Cookie{Name: "token", Value: token}) + + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + /* + Expected response body: + { + "data": [], -> array of data + "message": "Successfully fetched resource!", -> message + "status": 200 -> status code + */ + // check that the data length is greater than 0 after converting the response body to a map + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + if err != nil { + t.Errorf("could not unmarshal response: %v", err) + } + + // check that the data length is greater than 0 + data := response["data"].([]interface{}) + assert.Greater(t, len(data), 0) +} diff --git a/occupi-backend/tests/main_test.go b/occupi-backend/tests/main_test.go new file mode 100644 index 00000000..b9bcd101 --- /dev/null +++ b/occupi-backend/tests/main_test.go @@ -0,0 +1,31 @@ +package tests + +import ( + "log" + "os" + "testing" + + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database" + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils" + "github.com/joho/godotenv" +) + +func TestMain(m *testing.M) { + + log.Println("Configuring test env") + + // Load environment variables from .env file + if err := godotenv.Load("../.test.env"); err != nil { + log.Fatal("Error loading .env file: ", err) + } + + // setup logger to log all server interactions + utils.SetupLogger() + + // begin seeding the mock database + database.SeedMockDatabase("../data/test_data.json") + + log.Println("Starting up tests") + + os.Exit(m.Run()) +} diff --git a/occupi-backend/tests/middleware_test.go b/occupi-backend/tests/middleware_test.go index d8029bba..69b2f2f3 100644 --- a/occupi-backend/tests/middleware_test.go +++ b/occupi-backend/tests/middleware_test.go @@ -6,33 +6,24 @@ import ( "strings" "testing" - "github.com/joho/godotenv" "github.com/stretchr/testify/assert" "github.com/gin-gonic/gin" + "github.com/COS301-SE-2024/occupi/occupi-backend/configs" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/router" - "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils" // "github.com/stretchr/testify/mock" ) func TestProtectedRoute(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - - // setup logger to log all server interactions - utils.SetupLogger() - // connect to the database - db := database.ConnectToDatabase() + db := database.ConnectToDatabase(constants.AdminDBAccessOption) // set gin run mode - gin.SetMode("test") + gin.SetMode(configs.GetGinRunMode()) // Create a Gin router r := gin.Default() @@ -57,19 +48,11 @@ func TestProtectedRoute(t *testing.T) { } func TestProtectedRouteInvalidToken(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - - // setup logger to log all server interactions - utils.SetupLogger() - // connect to the database - db := database.ConnectToDatabase() + db := database.ConnectToDatabase(constants.AdminDBAccessOption) // set gin run mode - gin.SetMode("test") + gin.SetMode(configs.GetGinRunMode()) // Create a Gin router r := gin.Default() @@ -88,19 +71,11 @@ func TestProtectedRouteInvalidToken(t *testing.T) { } func TestProtectedRouteNonMatchingSessionEmailAndToken(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - - // setup logger to log all server interactions - utils.SetupLogger() - // connect to the database - db := database.ConnectToDatabase() + db := database.ConnectToDatabase(constants.AdminDBAccessOption) // set gin run mode - gin.SetMode("test") + gin.SetMode(configs.GetGinRunMode()) // Create a Gin router r := gin.Default() @@ -140,19 +115,11 @@ func TestProtectedRouteNonMatchingSessionEmailAndToken(t *testing.T) { } func TestAdminRoute(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - - // setup logger to log all server interactions - utils.SetupLogger() - // connect to the database - db := database.ConnectToDatabase() + db := database.ConnectToDatabase(constants.AdminDBAccessOption) // set gin run mode - gin.SetMode("test") + gin.SetMode(configs.GetGinRunMode()) // Create a Gin router r := gin.Default() @@ -177,19 +144,11 @@ func TestAdminRoute(t *testing.T) { } func TestUnauthorizedAccess(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - - // setup logger to log all server interactions - utils.SetupLogger() - // connect to the database - db := database.ConnectToDatabase() + db := database.ConnectToDatabase(constants.AdminDBAccessOption) // set gin run mode - gin.SetMode("test") + gin.SetMode(configs.GetGinRunMode()) // Create a Gin router r := gin.Default() @@ -207,19 +166,11 @@ func TestUnauthorizedAccess(t *testing.T) { } func TestUnauthorizedAdminAccess(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - - // setup logger to log all server interactions - utils.SetupLogger() - // connect to the database - db := database.ConnectToDatabase() + db := database.ConnectToDatabase(constants.AdminDBAccessOption) // set gin run mode - gin.SetMode("test") + gin.SetMode(configs.GetGinRunMode()) // Create a Gin router r := gin.Default() @@ -240,19 +191,11 @@ func TestUnauthorizedAdminAccess(t *testing.T) { } func TestAccessUnprotectedRoute(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - - // setup logger to log all server interactions - utils.SetupLogger() - // connect to the database - db := database.ConnectToDatabase() + db := database.ConnectToDatabase(constants.AdminDBAccessOption) // set gin run mode - gin.SetMode("test") + gin.SetMode(configs.GetGinRunMode()) // Create a Gin router r := gin.Default() @@ -274,19 +217,11 @@ func TestAccessUnprotectedRoute(t *testing.T) { } func TestAccessUnprotectedRouteWithToken(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - - // setup logger to log all server interactions - utils.SetupLogger() - // connect to the database - db := database.ConnectToDatabase() + db := database.ConnectToDatabase(constants.AdminDBAccessOption) // set gin run mode - gin.SetMode("test") + gin.SetMode(configs.GetGinRunMode()) // Create a Gin router r := gin.Default() @@ -307,19 +242,11 @@ func TestAccessUnprotectedRouteWithToken(t *testing.T) { } func TestAccessUnprotectedRouteWithSessionInvalidToken(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - - // setup logger to log all server interactions - utils.SetupLogger() - // connect to the database - db := database.ConnectToDatabase() + db := database.ConnectToDatabase(constants.AdminDBAccessOption) // set gin run mode - gin.SetMode("test") + gin.SetMode(configs.GetGinRunMode()) // Create a Gin router r := gin.Default() diff --git a/presentation/Occupi/occupi-gradient.png b/presentation/Occupi/occupi-gradient.png new file mode 100644 index 0000000000000000000000000000000000000000..21c4fb2b3e4d1b403fc524c32a6294fec8ea3a22 GIT binary patch literal 15993 zcmV-|Wg^2H3F^gN9goMo;KmvmZK171| zd6LZl2(T@d_dVEWg<#tCVc(x@cybEO}$o=^O(FdWis0{yC|X5;KqHCH)@@Af{2a}cja zY2k{YKNLV67eJam@R@C%l>Mklx@1vdDBB}t$xzL3#>o`rq+okdBjs${%gy7T-_v&J z%?|vhdC!h=iT;u$x8Om@Bt{Wvv2r{qDOX?`R#KTw{d-z+aQ<7{4`$#vmSYx3%I^vD zJ|=me0NJizEol}6Ro0hsFW%qrdC!~bY#*P6I>@i{-BD?gkHxz2OSsryA;*15yAWobmnW^K8;yure&Oc+)VFlDp>Ci*zTe4{(-~U%%BI zN9B{mr`ssvlLk7h=v%QeI4Mb^!$0~zP8iR^aU{nSkp6LQdzUXDjSfk)^eW4)Hh313 zN!cA~ooCyGlnw$r3I;uBUokF}Wj?&Ix(vv~4!JI5j|VMvOaO_@34~Y*2fs_j-Kc$l zqwnF1UGHhanf-SM&m2}7(}<1`l>uZ@>nK?s1>(d=TOh{&`}!a5f@4&g9!US^;HUe9 z^at@&+wo-cLdbgx9*oq>=V^reul=7)0%s}m*|ZuejdRIo(p*&OQ_8)YM!I4`N`GhP zh94hOglE$N34NhxOADlMwBzQbbQ|Z+yPG`vnasx{EPO%=N4A?8xD-cOhXRNd&1RMd z2Sq7GggN}#2@CFcXZufb(B#sTK>D@vh2%JzgiSc6Gty_h&F-8|=1+^V*3cjhXpfLV z=oWBG#+*sxcnADJnaNY;ny$B;Mku=L44HE=%3BYPcd~3#YT~8?(%&EG9>j3oQNKvJ z(_7D-wREDFlSvtmz}!YOy+Zj!hP#>oAx6OpXFHLjB1c>t@Vl8M@O{t$MK4LA1UA@%b+NaQeiyXD6VEq-lUeCcz4CLX#l7 z@z=cXi%)f)@S`j=u}mI36KM$H1YtjE4jj;-&bFn``_%)Va4asa7slCErcS_)lh^;) zS?V*p_y1Xp*|LXA){Us^kU9dKVp5Ep)b^86XaZ>pAdy+%E#VBKnFnk}FrMgaYu=EU zKrdQ01fZv=YgRJZe^T2s72VWV=l;yWhry!xL(O~Ax@Y?<`Rty#(|T}>l0=3o6N*ed zvv}G@|q!~==H|v7m z`@Xj79OB(`rzMG%M*zma_c~A6@UyC-G(3+uYBK_3m(fr;{5>5C^Yn>q^#Qe}K$@I8 zvj-J>TDh7a(z5lyK(jiNBxGW^1!HK&qS}#`&dF^*8G<#GUlmY832^XyqM*-dT5>t` zzVLIaYVy_$NRxA)7{rTrOoC(YiLK9cLKDl&2%?Y;8K5LC!w5^lz$tA%PD2w&22E}2 zx-j}{;OIIRRZBIoAQyTQZP^^Gf~*OUCg;vbA$WRBsGH-h2RfTs6OlQ?h$(|lqeueH z`ocI#0obI#2ZiMtK+j481@6)1A;@SydoWsO&GS%TR@);aCtv5MK+MUa8Q{ zVF8Hn#t4^hZAK6UlEL#0amobXM1^9;c+?HM1BnJs)>Q)%-H;GDsdz(FsVaKb!s4-0 z!#~;!O)S5h|M(a_bkjgF8@k_X|MZy1hfU0V#NiySFV$!98DF@GMwvPet01qS#SDw^GpQisq;h)vbr^)P($5@DuK|o}ZSR$FQvvrN? z8m5uRA9`i(<0)ithjBI_R`7qO_*~RTQfVZeZIVc?$qY1^gm{ReN@T1tc^bAHsI$E2 zePkQn*nTw@Q2JL)57kK$8?sYKohNXdXQ=#7{Ro!Ic<~;nqcj9aFBLw%WqzS$5^q9M zfdzkrex!kdpsZe+;#B&{P|j)*q1S&o|Ixlz3LonsEdi?_^gZg_@r-@$J0EC3Fn~8V zZh%8jlEFCOl#47i7PX4=mYmEpyCV*6qd1>I14!JZb3jrc;QQA_;h<%N$8LW9NP7AB7AA53+Hf8G#cPtall zW!iQ6R*)CNg47_WN7w11Zjqi?@3No~ixoK1cLf3)0tKZNyNa1g8RFw0A`{7A+;bXxX zIwdqZ9P(R3nYTfM9E2qL`bL8({QpbsZa zU=P|4Vlt1v58>;Vn5ZO|XnP?I)r#-;k|vV@OjY=b;G}3u>aZDs8xt-JLp_8+w=y-n zz9IM#!ey5_e+?!niaHzt5U6zOST-$afMYr{0p+|D^E2 zn(6;%3hyVsh0I#^!|a8IC6VbSNeUvAldb)~;2%;PSy*K`(|&0jK{OihSwbbSL~5)` zccCgEwODKnK@(NDC8J^%{2^qrEF?P=1%@K;^wkra-D2qSvyv zb7rNXM&S^S8OOe%QIjoYj|d!X6FKSYkaEFu8(!|Qn7CG`AMequA&+rK!l-2Y-ZW{9MI$i_l|7nX>hqx2btH!h8g2Y@ zg+J|s8U(LMUt0sU!Wf3wK3DbDTAQnBAWWDdN4Kw{Su2H7i?8*;wl1? zWywqsh6~*(j||($M_aPa(LU$Up(jr3f9T5DMp@0Wv1v z8^sBqRO7+Pe3WvH=-!W>5&q?x7t7(T9io#HsuiNLw-N5~!Ad^6ERcTc{wam|QQbDL zm|&bzJj8Nh1sbDHwyjJ~5BlXYqT|f}Xc1)9?Nz2lWlb|(|WW4Wt`#&06?z2Y)(z7}X?wIJz@Hj9d zJhmMw5Dz0cO-hO=G(Z-(HAG2YFwH?lkRE0-Vta+^=z!u{{QAq7at+v^6c*<1v=0L0 zkDwXBEd0<~`9_Ni8&bZK`61BTEqqyS>F65C@&}9l5Exp}EhJ8_A_s-}H`Eb43Jnm_ z9+u^H%fNMo;5?Q2xVRdvq-zrxjMce>gM``C48t-`tVNkwsb@UPZV$gS0!<{_L(gY{ zSyP8-CCR#e;GW)fw9hRGq@TzSZo!$I42q$Ib`FQCAn1BbjxVhlGBsf+Y=Sz>4~|xMq+GGXYvhajNDAE4k6CIERiJzw=xI4R@r8~xD(ZH&RjT? zde%dsQuAT?S*0-;uZ#9^rLh^0hifLwEP;+2vvB-E6`Pq_1#i?&M!tw~64j0mV0BgU zSN>{Nf_!}t%UKDx;)P&TX$au)Mnpu1NXSd$cg-4#9~G-bH?Im{@NsfAHx|~_ zlx87$%KgM3opRqXS>|)doBaNJu(znLZ-|iDaFML;uK>&3+S$0BvHuR(H_Y0q#zz=nO z-#fGCGxpOJPogK?PYq(i*9i)yf*z#0#8@QGV)bdF`o!FyN4dqi@C%|0R3%0Ps3Jo^ zM0Gf2Y!+t^207G|QZ+PHyEdIu%eG*WPpL~$RUW2ks)S9C?LRwq&W6ViIo=^aLZ6~r zw?$EVO69sk^;wN6LhdNIZ4Tbsb~GC_+Z||-e6|h2uyAHh4X)xR>0*F1QiWQ^8mDLv z!RMf3xvRH=re9CGpBbbcMWt7QG1seIR;r1<=blBtNg}mD%f}-sQ5RJBF;y~JZ zchFylfn7Jol}yf43!m7D$Gsj8-`8-h1U!xZIl6hQ8ix*wHYx#yMLt~t$u}uG))m;Y zpnr1Va|j|iD9g4-G97G@_FxF2Cv#AP(D|-IUkz_xA09s3R{U9zZt#-e&pI*4Mpbd~F7il&ECZA1)d|8~Wq|>leXR zVQL*3;QjE@wH%Y=vHi&y&}KXgwX?E3K=(WT_K>+kztBz5&@i6MKF^2FMmnFUDX5k3CasZ` zx$gg5_`4O;7~fqu3z=?OoGGRbh8Gg$+Kg`Y`i1pl$g{;upP&I(RnHUiEpyv@Yv_ad z=v*>xGnvxgs|?iy-t?if+R|C5qoiaq5}xPOP#$xsEUG;jIYXrGIc*Q6%isI>{P{vR z|IlWWMpFv0>kI21I)nx&i`uMs;GugiYe)~E&xFURcxnQzcEq-?nTkBah8rF#WCl z7bJuCs*)ER2hGr{>IE9pBl3|7NKedlM=3*Z@+-N|um4fQ=zGtnR12TQ6O2FUer^yN zBshzv0*0GzHj~w*KF(_Ub{04vF<~!8MSQAtVPjtSR2R-V6o?FaPTUoyKrT7E_1iVk z22aeLn?k|oiN|`%g zVg8QggaKoj24cms5ORKjop^R@x~Aad@wxLec$1Ro#~}yCx1Qbla6f4bSY`P_+XJHo zpe7R-@>AAd@}$Lh1{A4FrMLRpRo+nWeTRQu0SQfDSHNI;E;PKgo$^(rHKI)^8zx=? z>MZAkk>`*+uLp&0Jl{mVK~)E#cQbpIK(Dj+8azUpS{=(8S!h+-BL_9eV{?C*BE}yN zlyjNg`Ng(xW?>Dbx9!2)7uS8g6NEKt%1AbutOV$sp{cA`N+aQ$Vy4haF0AvjunO|| z*2e}EXCth3D`aIF0VV`NPWwlCAi`i(Tx;~$!g<@mbM`cTJ{FWxK#(_6E&!W4JN(Uo zTD8Yxb6=rUN|HcS07%eHgeH+Mtlx?LAr-gT42qh#R8qp0@{#aETcnbSs462!nq|IIIJd5KOL*)FX=;Gf_06-*%Y7y_cR}>=xxdUvpO0gTAOgVvYAwWy zWfjbdJ^I!Kai~EaJ$O+c`lM*yIf6(u6^H_1-0qjEcFJgC1tMaLETU76(Cj>wPe$+$ z?WR;<0#;Fa+tagWw~lt=cc<}$Hwho69zS?)-STmmCYez;A9OhcXW=D*n#%V_zJ2h* z;Sj)Y@Lb|5TOZWnFg+s`AX$(5GA^83%-V}$9;reoWGlGcZOKo>d&~D_) ztQ&Jil@&tuzFI^h*{xm-WZ;x8PuMj=FchUwdxzAk7Vds*?)+40E*XUt3Wo0FC{y`^ zE@YSTh!TiL-?}LN=)tdy*)1+rSoX`3AH-CVoU}}BlL8L|;~(kTBHm%Q_+!<8iMIec zu$-9cq23MmSD*h{MZq9oFA-y+Y$1S&Qlp2zms`S)1QE%6tG#9yc23(j9XtiR(H3cu zWEfgrcxvCM}?KazjYfEXfpOz4if=8 zX~RK}d7IGzW%T2&S&(%C4-=8mR!&J5eJ%Hn5yUu z#?VA_DtP&(yz!9AQ})>WU-aQSN03dMI%nO(YrY={`6y^!lq1SCGe3hSYRGp*wzs`H zGw|&LeJR02((Eg%q}ZC-q%=(*9(p;8YI_2cl-VzyFp^#F*r~aG%wfSW2i|RtjkE@H zdfM(;dsowUHBLV|uW^9OAT=uAxkIIgi zy=6-YRc7{9$YJ7W6mweH-U`68=y@Kv%%N0+$+y!|hZ+2@_i&)34_rx1KC;L-AvWC6 zG~>LKJdk&y;Sfv*zca?n1T?$!a@K#|y1`<~$!5r8s_0HaQ`u|dgeGz3JC^Uqk!2=e z;3ITV;)7hDZyoGMzlQBAOPNw_&L)*EROU?oK|gZPtKJwbtD5#bc5XeiLht%Joo~%u zp78}wQ^p-APsY*I?Y}DPTUPSCwwq>B`KzL8vp%W1+18~rr_mA)@i+t>WOYTG?l5c0 z9GEyX(Hs=vAXpO6!e79l-)z?F#w`K4)Pd{LRD@>EfLyT* z919|?4z)B^rEPS>2fa`>5Xv={#KtSr5r?O$>6%*1%^x>EN)))}sqZ5is8*OQ2S{rF z6~nY8gV|wG4F;xz;^(kC>duw*w%?jVFm07cuA(-KnkBz9lgUSux;X;|&htm67n)Jf z1ncTE*__p@YAq@Qlce}%kXi^q!xGi?l=C)TN2_gVq2w+f znfLQHUfuiUjn}4`eoavU^L9(oyxC^82&=Mr+c6=w4LRxSypY<>TC#@c5zP!uJiOmY z;n*g%U(bwjWfE^n-^2TF=tf^@r{{X(7q&Y&Xi_s%8<~{srxSQRSPKoVuREB(EgjYF>1H098_Y3i-m;9!LYk+ zJ3Rc*n>S~vTN@9_%z15JTNOX`fw%66+l+5RTBX}WvRU6R3|-iMb#A%O_O=hqKKSM^ zO~@oKTTF^NH#Do;J%!+S6VeV3?;kQI9^lIx23IvQm4fd==p-T7=}=b~g7KZSFo**! zkN22-=&jo)gkZZotC0QbhHLA3uH?@Y_z9#Tx8aw^eW_^?b(-YIS*`I@ez=o>vh2Yu zJ`O(X!OwLCFWI`TnZXFXyrDYdvqATX8^&6zgbk<4Ay>?8E;AoU_~M2X^m{*aGitOMV-O}LURV4n2Bw?^=I zu$NJjbqTZhxmonJX1U7y$*Af*#AFOjR%8J`%M1srgI2xuHke+6hn%JkYNhW(MWs9h zocs6PH-(>yg&`cpm!?P~?IRAxmtJ*6UrWe0_B%1=^EkJlgIXywU&hdvCqID}@JX{~ z$6TDuQDq>$Fv}U6!f}|fEI!HR3sud&qlqv0a z-nzRBuRn=n(gQ|aD89z9HEMwxXQeirm|mg9n`V~<12`r%kES=F(n>DahKl~Cmo}P3 zj-Y!Z$Y5EBUSbrWOt6Y48|E0Zc)*wd)s1h;rgc(O8UdkD%?+j=ncLANe4qiL?0Q7i zed%wbwc>_d*5hQ{6{F5eS!A^+ksfjCN;}LRdh6yXbmp?25EEg)kgXY5%!=wsDOMdw zc+)xzIk{->?k}Hk$pk)3JdhtALS`V&q}_+Nn6!cn6bFTxYHgXy>u}91vm)aRfqIc; zYof}51!k=Edc`96pt%1w1fPPLP^(?>_7M#1oIV(huUglpOCpeTsc2bqAmqFWJ46@W zk02@oAu}L{Vqz=!i&)@gNR39Tpec#Vk5?zpd zBY%5LXY~j*P; zdaGb4Wv5LO5Dh5M^O%_=qw(2_5oBHndCvw2kMy?nRy;;j>&3Z6N9>^AZQl<~8SNRZ z^dM;Gqn<6t@I1Ge3oy&!jJi~4U}gm|H!tbWO5^R7@Wje#_u`Uy^S+`vr#^=G=Pqt> z_KmkNM@lpch+4_s7#lx6SZo^y9pRjsp7r|@()B&smJjc+wf>{ zBm*~@@zHq;r36UyOWG$2mi(Gi2(V`IWF}#)65#kazD`)9z3shKPcjZxPAf~jh|@-H zWtQs$|7oQMv29K(W@GwxGXN0)!z^o62~)GmSF6>sCBH!?S(Zppqq+}Kx54|0Bcf-c zw);9{#*h_|)4+5IPhYUH?+CIH)$TU4M}ve_7K&!5l2EGTNNrhz3)?Tufzed-`+fQQ z&?jk$a!#!jRsJrD*G4%-;-w-%-;7|HzR_fP9MgSx8dF^I8T4tgE;8=}HGrpF3V2O6 z?Ghlg0&Pg zsO2bm$~o6N$110a21Nl2AE84pwGNJK)U$TdODpX43ej#HwQUdXyJrS0HX>bb7tLxi z5ttU~X2tN@u1hdcso2)Kc%U3wT0?oLgM799tK{d=&-h}0AI_&DtuP-6wT7fSZ?Oy8 zdvnk@p>{=5bwdHO_Majr9PkMGI^--b%OePMg3d2Qz{J2~qiM$4>U@IV4Qf3srIkG`AE-28_#XHRti}m+GUfO^T@&RDG^%Z#>~XkYyW*RZC0?! zO3%G$Lg;mJ-tOCXU*CQE9&l3l9GX-JR9|iXa=IX`1U0zQ%`num8Kydg=%%DZb~?kW za2@nLdOe(~MPhIqitAD_p~Zx00H%O+01COF;R5}^wnBqn&)?tQ!WSm-MvUOJ8a!YX z8s6S`rGgG1vaPI_F*{h$2*K)DHG7!V5#pixY^1z|+>?JIY5HpiYvN*G&p*`x$C=n6 zqBc~E8|6SK(?&_S`}Y0yEJa21J5Hr8%u>foX&9Pi@|c)eNIT|iwV+QIlHutONWuDn z@_uR+jZEH?e++ym46Kw(Sxk`49#l^P;tvz39`mP1^52^Y7=NAEpgu?#VM*nT(>9H$sJh#VXP!Xdmx^G|AJ z?P!1G3lxR!9L_&Rg;;cxqo9hi+Pa4GPi!4%KPv}y5HgtzPDtU?j-romvia&Mb)hx6 zGUVVWPc?&oB*HRkCM^BerC7O8NDlvgdZjw%ip;@3r!}z$2+-IB8LEb{bfE@ttML-=H>KeB!H@Z;bEveu}jq)33G zH#hI^7MTAKttKWk^+`DhI4=+3LA6QvhL22W^ihQZ* zi@Jw@`$UwrTEEe>s7Us;e}2&jZ8-luBr7@P3Ohcx z5tp~`ig$dZy$BBUv8RF^mQ6n#W|}O?Vr$-Jm)7v0?moh@OM!@Fp#5_cr6y>F1fXNC zdjPS);ry<_r9N}ZZ^xo{z47!gg6Q`+;WE@JkEvW53mQ-%n+^y>_9^^*U(i#CR+D(b zs(LRM1adrTQk}_Is4TUb; z#n|(*+^H;zZ^88goG zSY%_w+9CxVS0?FmdS`X1i^h<(Mt^Ll$G!B``glt2c;lHEn%C>;?LzHH$W}y3Uh$4I z()f_R<9E+GlJh}CZI3TEblsW%dE&D653fr4<#54#Yo~Ssd5(TRMAPidKk<7*d9$GG zz$}u-K|A=iMnpOF5M>4=b%bSP0(uA9tMeu9dgF(*%cIgprVPW_eNz$>5ap#-(-YG7 zhc18TC$meOPsA5(MeyKpMlOB($LqqX-~O-9#h7KGSymbU8R@g_SG+5+suA8Tzk7+c z19`y|EKXeYjt?ac@q2VX9L{91Uy^DqO#N{%_j*Fp@<_h4$IwJ|AnZq~1rf=mZ~x>F znxz4u%#_*pJ}CIpV-T46DI^=WUiP*>ODqK<64Z8T%PviZ+Q>feifW-Nkt^Qyu_>>> z6SVyqp0DaNaMQnL>UR7}1oNm{ioGR;)<}ms(1OpHfio<3(0I}!exx6oQ0~e!49lr$UPkqNu}?Wc8K{?600l^hSpZ_4qIR|zTPI=vR}C6x0xucl2AuG(^Ha~btuz%2#_SVCPk1|&_7UW#A(Hb^G~Os ziR7;QPg3N+;Q38MTWdGeD7XCXXK|srwH5a!gVIuOwhshLT=9<6s+vd$Zd0$L(lzb3 z{4NuRI?6zMV)h^x6flqz9%12d#>r<^#R13fjH%4OYK0h5X7#X;-pc6>-qaS|jAo|L z{TGdiBTnN-(|6{dSyRfNoZvfNe=Z}G6KqYpOm4lbtv09g&U~iN;`{}Djf@pLxSAP9 zVDLOU2UUucx}$Dj0yu=~ZhjmDlXNNiWCW&YtJl`>iGI`n+f8-w(c&1*YohuPATjXw z8VW#bCF3tJ+ISd9g`vF{jCR|ZzUz%=QfmRIJ5YP9-+00#s#L1?p>0kRSGS*@gDQng z!R>EkCWQ|pVD_6en;nAI#=Td(~GgEu;229_T z|JmRzzkN0e4Hl(^@|j^Yzb~kyD6_^7zG>GK{@(WbFM4>`dX3DrYBiIOXPKmZVmFy{8o1)^T{UgkeA^qp7{){No0wkbPnE3~Gd5@O z%i2G1$ob#;`b#4Sh(6UxM2Hiweb)yn3bk)}{gotjpH{!<^`La>iEG|@O3hWHM-u{R zXu+Svr<7V5zvkbaRQFxYZ<(Hq`nLZY&=vH=k?g##<0R!f9+CE-YDwc{vY_3K>xo0? z{VU$_G1O2W+l=|2t$4h&R7;QfFwWlT+h70r*ln+8dT!0XP;;b0;JH2j%dXpAe{rmB zqeXqM2dNJ?I(QSPBu}gbM0CtsUw?TFudlBxfKVo!LTS`=iNireGH-bfV`}$p>Udvq z35pQbR8YPm)rtu9yyk#lIdR%MXN4E~(+DRZd)1tB{5WyEuCR+Vxs$^>XgoKRT(Z zoLjYWQIh~z~q&^ZzZ%hp12y2JU zw5`A_F9Io%t>nuTsKPd7!@Fs5Uva&^Ug=p}_85G*`3hFH=uMXrakU<6FJ`};E85>* zvpG;8qIP`61g*)@>N1%b5whb)15qG1|LdNN%E}8lam^o9c7tyGmsc~4ZmI~1lT@vH z;=2EMauymSH%yZzLe`nqJ9skT{FnNC*N%Vq+a#X-BYOQI1rTCl@J~)Ij)Q&p2avhl zi@uHlSWu5!>ddvA5LpI#C5kdXM5kkTO*KMD_z}aa)~|Jxw{!v88JepiuPL=>D~Mh5 zZ?;Z3n>$yOk|7XDD}-5UP|>9q*9O5=mS|EP&Z(r-W=8u=x zN@!*#K=r9Mi&@lUQX4gAp+R!p=99BG{L`KZ{Jt*gWKys1lAh>cW*Hdm#*FtYUH}+QLX@P!okqF(&Ld9 zx$&R(5P0GOO+7v?uO&UaZVuPWAYrF+94^kc(9*G7mlJsA-)l%eIs5}iV7+=!tnqvy0}lPLab zKokgtyL2qNL6y9pv;)3>mK;>%t5mrF_$+e6Ki1z7E*F2!8pU(nM`gae)m?FVMl6Ul z9*OFyV9_I(~~5t0B>}OO}$B^tMf5Pu4YG)JU-`ZwxOsO2nD)h-byQ;H&2| zZoy4_?cbkRkvThDeDKm5m@<0fzr0ov87o}zw4{1>yb*LWDB3~yp*l{gN+As-Jw{++ zeu1qhQoQbOcPHt3iBA*i+8KqUM+7~K0!gTS&UCH~4`2$ngagNb+%)~SgE({II2$Eh zII2_vkpdGJV!N}sdQB7veHL#}PHC7o3l;G+m<&z<^?VYlTRzTc8cK*e|GTkT34?-h znmqWOH3O-Cax%hvoREqDkb42<+dkV+f*Y-q9fE(#WxY zZ?G2P{2HBWmpNd3BB5-$0g;Cc?0-I~CMNuFK`pnJ0D;G^C<&|045ZC$rgNd2L7FLk z-D|IPL4$<87ftyTY3BKPQT47@*ZuM}>cZErn#GuN$4NA^<9#PB%H=It(9b+RN+vaG5D&O;RTH~>bwKH2GW7SaqF;opxW2)9)~g$b8erbt3Uw3)t$C&KeB#3@lj~2!{F;ZFOqo~}`Op>s z1g!Y=f78Hhy$t2ZEO7``6)4$YQ@SgPlTweNr+m%a4T&Rtqd=Af5|M$z6Fbf9mVs>- zvbpxsa3;~D`QR8WA&cQY{qDig;bH1ZMY=e-2&4k4S`Z;Ue^eT+qM8<;Uv=SY)NSVv{^kGfV(+Tjt+fsn@@L>#QE;Som_ zB85Fn=SC*Ygx1T|^Bv5y%xYw^0I6|jvI(@3CmdyZ)s2-! zt^;S#v~$YR_IxEm;WYVQ$j?-rcCaBl!aUd)m|v}ti0`84SSCp$K%K=zibvI+*Zycv zj}S7Y30xKJGC`)BeB^>(QG$9|AUW?rAH!w$2z|4dzG#)9Z)kg%<>b1Tk#R+bP)8L| zG#4g?(GYD!!i7|67u3`_s8UEn0QFAtiq5un7d)e-(1cv`(>jAmX==HoKX=V8vTenx{V^+4FpWEmw$sCywrkq4Iqhi8-U&fF-)2i3f@rXnrG?*rv9eZaeejiubpx2uMV3`2{`z#fxX!eqzLN?aQ^XrYn)JW%hIf ztz^(-W6}B5Q3k8IcBvxqL47Z{wQos*)4dmO)V`wAt^vh@Xb>mG;MGsRI(F?dD;wL8 z>*{kothZHvYBF-w_xGgmJDoBr!uXepHyXP2)2BL6g;d;O+&_>x-8v(?$->Z~R$dkbFn(x++%6XR5M%Jle@NqAi$g<7b`MBzo>SBLs?==RT72O{-d)x6ok5Y@HZ zNxbZ+`MA_)0sS|2ol=(+L?!1?aJ}C{X{!WXc&Te;ayOrL%4TSgT>bQ(ZoKQXG%0uM znJfRED}V561|PbZZfI8Q+#z+pHwfyeII!MZK2yE9zWj;ZDO3MM%^uRgEM(Z-ek#Rf zQWjfB7Z-W}4BT-}ZTGRNo=4;?a!+>4Ac9lr2&Ik(uX?s|OJK@nVNPxd@;^{C4LB&u zzO#llQ^5S=Ha&xR7V01i%-)XLeMac_keUEfM}@@jFqxxjj@_SnC5n+{QkzTS z*+f#fE~QN3U?>P3nJJQWAHHX6tso+)x_QvQhq?)Xidh#j88m4XGci#EMmDP6F)P?QFkEqCFs7P9An;u)&>NlJc zKn=V_mU4fFClPw~x{cf156wgpXK@EXWIN8q-s3a<;l>AxAv|)jbuZOf^4UA&9l2Z@M3$^wtA{L zx=3Q^n!EXo<(bf0MI17qrk9akeu$IXZQOPZR9;Irfs4BPf&`|kQm-UDrurUHg4Jvp zY8*iN`fw<{)aMC2cxJTaSvNka#RilVEn|*#vPxX5fV#l559~nvYKk!P_vMd_^xLN-f>^mCV3v~p5vIMi*^iK@j z{gwCDw3uTlS@FPQ^g^7-s7^*oP5Cc~^wmGu)0iSST_U2LQ~Elsrez~HKb8e=M41Cu z>}Vv2NbdZLQ-`SU0K#VkN0e&B&s_{eBpi4K{iiNH_RJLWDPmEUI6sx@2xx^_;rWSr zgNUSN(^f9GZa<~>iYI?HBN?Z@iHa2PrAS}-medJd?#2w*8&w2AA4XYp{7b>tWyJQK+B1h{4^&(S4A%NNI)->7GTmmO`h0&p( z7#V2HM5vo7xc#%I5=%M+#eAHK7RIl5yv~|Q!Y&>DGblQv9^@?7ue)DHrUNyfLmE)H za)-IWO(W9v(Hg*LP}`)E*4nuS6P@W`#HFIT#({`r#Xi#Ea_OUcx-Bjqk!Cvzve!5t z`fvMU{Rqp;Q@d$XzaC%hUMQw~Xc-;pUD+bQ#pKcl(f?ye=t)T5bpe|2CYAnS0z97t z>iI2;yB!8AoX~<#ZH8gT@W6ENy zk&AEN-N#hN6*-G1);n_LsSTkM>LnLluY9;crn`SpZehY>viCt}Z8Yer0x1xTGLp1q zgJk54EB`6SWGFd&$?`pi4ml3_2R45V)D~c37qi1vYq)-AG9p_GDYeNnaxJdqZNLAD z%KAjEm3;NuzezF9F|)5XMZqx~x%Z}1S7qbprDRQj6iEMFs$r*UYN}okFd?KrdhZqQ zOIP&y+h0ja$x|}e5=_V$4(K0TUD>R>_@>=`m<}Jrd(loiD!HD&W;41H>F=M6xUCCg zYJ*0-9pfyzp3eO@uTCx4(PT}56vzOEuQVXEL$=NZshz2_$TyDMd)28+)<$1^5r z{r)?eLwhy_kODzdm8AaNI9Od^S_2X=(DY@QZ;cnEx3?T17dfxSnI$r&$+6UXVCj+o zBvBlE|0unmz~lSw77U50uVs>)Y17vd{fg$+LL^_gbax+481#vFM5RKc9B=VkdPhc@ zCs1iBAO+IzAfd_GASNZ_g0_Q~g^U-o-m;K~r7&3sMjLZD4S}?08_%n@J z@g4Wyz4p9Sxe$O377D~;^+TY6wCrM13_SQ?bNg^4$1IS_g+OH$cps{zZczQa^@`CV zSqCMQX=Jf1rWBk9?rqGzAeGAb=kM;~9HUi0?J7;+soL4|EHHlv0hEPfM2-s}l?ytk zNrv1Mm@qkT)MZe5%8FWxZXGpLA8oZl|d&IQETJI@pt_3_CEAbef%f_ z&K&c)F#Su#`7!*;7=E#?(_*ZoHJQv}Lfd&LwYC~^C30K>DG&q{-awWj?W7~P060>I zbB6#6OvSExX&=6S3Vo|70sah^Vay2%wQF{m+d1D~V3wt~wl4?|eKi)5AR_qw2IT7-vyqT7xai_1Ed|$TQjxj)`60Odoq1=x{l%fkZ;uNqU&kr?i6hmJAhOz*B1B zSJYC6Yb>>ihS4DEcc#~z>5u8R)4_z6Q|a;Mr68)6 z;~q$XT#zJ^6Qev#&XS2y)tiCclfe#6CC;fFbY Date: Wed, 26 Jun 2024 19:08:37 +0200 Subject: [PATCH 08/54] Validation implemented on one function --- occupi-backend/pkg/handlers/api_handlers.go | 19 +++++++++++++++++- occupi-backend/pkg/models/database.go | 12 +++++------ occupi-backend/pkg/models/request.go | 22 +++++++++++++++++++++ occupi-backend/pkg/utils/utils.go | 9 +++++++++ 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index 7e572636..ebef3cb1 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -8,6 +8,7 @@ import ( "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" + "github.com/go-playground/validator/v10" "go.mongodb.org/mongo-driver/bson/primitive" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/mail" @@ -55,7 +56,7 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { //link: https://cos301-se-2024.github.io/occupi/coding-standards/go-coding-standards#response-and-error-handling var booking models.Booking if err := ctx.ShouldBindJSON(&booking); err != nil { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected RoomID,Slot,Emails[],Creator,FloorNo ", nil)) + HandleValidationErrors(ctx, err) return } @@ -177,3 +178,19 @@ func ViewRooms(ctx *gin.Context, appsession *models.AppSession) { ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully fetched rooms!", rooms)) } + +// Helper function to handle validation of requests +// request validation error handler +func HandleValidationErrors(ctx *gin.Context, err error) { + var ve validator.ValidationErrors + if errors.As(err, &ve) { + out := make([]models.ErrorMsg, len(ve)) + for i, err := range ve { + out[i] = models.ErrorMsg{ + Field: utils.LowercaseFirstLetter(err.Field()), + Message: models.GetErrorMsg(err), + } + } + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Invalid request payload", gin.H{"errors": out})) + } +} diff --git a/occupi-backend/pkg/models/database.go b/occupi-backend/pkg/models/database.go index 58803367..5739c200 100644 --- a/occupi-backend/pkg/models/database.go +++ b/occupi-backend/pkg/models/database.go @@ -18,13 +18,13 @@ type User struct { type Booking struct { ID string `json:"_id" bson:"_id,omitempty"` OccupiID string `json:"occupiId" bson:"occupiId,omitempty"` - RoomID string `json:"roomId" bson:"roomId" validate:"required"` - RoomName string `json:"roomName" bson:"roomName" validate:"required"` - Slot int `json:"slot" bson:"slot" validate:"required,min=1"` - Emails []string `json:"emails" bson:"emails" validate:"required,dive,email"` + RoomID string `json:"roomId" bson:"roomId" binding:"required"` + RoomName string `json:"roomName" bson:"roomName" binding:"required"` + Slot int `json:"slot" bson:"slot" binding:"required,min=1"` + Emails []string `json:"emails" bson:"emails" binding:"required,dive,email"` CheckedIn bool `json:"checkedIn" bson:"checkedIn"` - Creator string `json:"creator" bson:"creator" validate:"required,email"` - FloorNo int `json:"floorNo" bson:"floorNo" validate:"required"` + Creator string `json:"creator" bson:"creator" binding:"required,email"` + FloorNo int `json:"floorNo" bson:"floorNo" binding:"required"` Date time.Time `json:"date" bson:"date,omitempty"` Start time.Time `json:"start" bson:"start,omitempty"` End time.Time `json:"end" bson:"end,omitempty"` diff --git a/occupi-backend/pkg/models/request.go b/occupi-backend/pkg/models/request.go index 30cc9f29..f3ff6973 100644 --- a/occupi-backend/pkg/models/request.go +++ b/occupi-backend/pkg/models/request.go @@ -1,5 +1,10 @@ package models +import ( + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils" + "github.com/go-playground/validator/v10" +) + // expected user structure from api requests type RequestUser struct { Email string `json:"email"` @@ -11,3 +16,20 @@ type RequestUserOTP struct { Email string `json:"email"` OTP string `json:"otp"` } + +type ErrorMsg struct { + Field string `json:"field"` + Message string `json:"message"` +} + +func GetErrorMsg(fe validator.FieldError) string { + switch fe.Tag() { + case "required": + return "The " + utils.LowercaseFirstLetter(fe.Field()) + " field is required" + case "email": + return "The " + fe.Field() + " field must be a valid email address" + case "min": + return "The " + fe.Field() + " field must be greater than " + fe.Param() + } + return "The " + fe.Field() + " field is invalid" +} diff --git a/occupi-backend/pkg/utils/utils.go b/occupi-backend/pkg/utils/utils.go index b119e901..71b34046 100644 --- a/occupi-backend/pkg/utils/utils.go +++ b/occupi-backend/pkg/utils/utils.go @@ -7,6 +7,7 @@ import ( "log" "os" "regexp" + "strings" "time" "github.com/alexedwards/argon2id" @@ -164,3 +165,11 @@ func CompareArgon2IDHash(password string, hashedPassword string) (bool, error) { } return match, nil } + +// Helper function to lower first case of a string +func LowercaseFirstLetter(s string) string { + if len(s) == 0 { + return s + } + return strings.ToLower(string(s[0])) + s[1:] +} From b3ea34c4bd39c8116f087f07764a84ae53d3bfd3 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Wed, 26 Jun 2024 19:26:44 +0200 Subject: [PATCH 09/54] Validation done --- occupi-backend/pkg/database/database.go | 4 ++-- occupi-backend/pkg/handlers/api_handlers.go | 11 +++++------ occupi-backend/pkg/mail/mail.go | 8 ++++---- occupi-backend/pkg/models/database.go | 12 ++++++------ occupi-backend/pkg/utils/response.go | 3 +-- 5 files changed, 18 insertions(+), 20 deletions(-) diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index 24b38db3..06584b83 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -129,8 +129,8 @@ func ConfirmCheckIn(ctx *gin.Context, db *mongo.Client, checkIn models.CheckIn) // Find the booking by bookingId, roomId, and creator filter := bson.M{ - "_id": checkIn.BookingID, - "roomId": checkIn.RoomID, + "_id": checkIn.BookingId, + "roomId": checkIn.RoomId, "creator": checkIn.Creator, } diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index ebef3cb1..c914681d 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -62,7 +62,7 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { // Generate a unique ID for the booking booking.ID = primitive.NewObjectID().Hex() - booking.OccupiID = utils.GenerateBookingID() + booking.OccupiId = utils.GenerateBookingID() booking.CheckedIn = false // Save the booking to the database @@ -84,7 +84,7 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { func ViewBookings(ctx *gin.Context, appsession *models.AppSession) { // Extract the email query parameter email := ctx.Query("email") - if email == "" { + if email == "" || !utils.ValidateEmail(email) { ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Email Address", nil)) return } @@ -102,7 +102,7 @@ func ViewBookings(ctx *gin.Context, appsession *models.AppSession) { // Cancel booking handles the cancellation of a booking func CancelBooking(ctx *gin.Context, appsession *models.AppSession) { var booking models.Booking - if err := ctx.ShouldBindJSON(&booking); err != nil { + if err := ctx.ShouldBindJSON(&booking); err != nil || booking.ID == "" || booking.Creator == "" { ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Booking ID, Room ID, and Email Address", nil)) return } @@ -134,12 +134,12 @@ func CheckIn(ctx *gin.Context, appsession *models.AppSession) { var checkIn models.CheckIn if err := ctx.ShouldBindJSON(&checkIn); err != nil { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Booking ID, Room ID, and Creator Email Address", nil)) + HandleValidationErrors(ctx, err) return } // Check if the booking exists - exists := database.BookingExists(ctx, appsession.DB, checkIn.BookingID) + exists := database.BookingExists(ctx, appsession.DB, checkIn.BookingId) if !exists { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to find booking", constants.InternalServerErrorCode, "Failed to find booking", nil)) return @@ -180,7 +180,6 @@ func ViewRooms(ctx *gin.Context, appsession *models.AppSession) { } // Helper function to handle validation of requests -// request validation error handler func HandleValidationErrors(ctx *gin.Context, err error) { var ve validator.ValidationErrors if errors.As(err, &ve) { diff --git a/occupi-backend/pkg/mail/mail.go b/occupi-backend/pkg/mail/mail.go index f5192a4c..1dfb150d 100644 --- a/occupi-backend/pkg/mail/mail.go +++ b/occupi-backend/pkg/mail/mail.go @@ -58,11 +58,11 @@ func SendMultipleEmailsConcurrently(emails []string, subject, body string, creat func SendBookingEmails(booking models.Booking) error { // Prepare the email content creatorSubject := "Booking Confirmation - Occupi" - creatorBody := FormatBookingEmailBodyForBooker(booking.ID, booking.RoomID, booking.Slot, booking.Emails, booking.Creator) + creatorBody := FormatBookingEmailBodyForBooker(booking.ID, booking.RoomId, booking.Slot, booking.Emails, booking.Creator) // Prepare the email content for attendees attendeesSubject := "You're invited to a Booking - Occupi" - attendeesBody := FormatBookingEmailBodyForAttendees(booking.ID, booking.RoomID, booking.Slot, booking.Creator) + attendeesBody := FormatBookingEmailBodyForAttendees(booking.ID, booking.RoomId, booking.Slot, booking.Creator) var attendees []string for _, email := range booking.Emails { @@ -89,11 +89,11 @@ func SendBookingEmails(booking models.Booking) error { func SendCancellationEmails(booking models.Booking) error { // Prepare the email content creatorSubject := "Booking Cancelled - Occupi" - creatorBody := FormatCancellationEmailBodyForBooker(booking.ID, booking.RoomID, booking.Slot, booking.Creator) + creatorBody := FormatCancellationEmailBodyForBooker(booking.ID, booking.RoomId, booking.Slot, booking.Creator) // Prepare the email content for attendees attendeesSubject := "Booking Cancelled - Occupi" - attendeesBody := FormatCancellationEmailBodyForAttendees(booking.ID, booking.RoomID, booking.Slot, booking.Creator) + attendeesBody := FormatCancellationEmailBodyForAttendees(booking.ID, booking.RoomId, booking.Slot, booking.Creator) var attendees []string for _, email := range booking.Emails { diff --git a/occupi-backend/pkg/models/database.go b/occupi-backend/pkg/models/database.go index 5739c200..ef6ae370 100644 --- a/occupi-backend/pkg/models/database.go +++ b/occupi-backend/pkg/models/database.go @@ -17,8 +17,8 @@ type User struct { // structure of booking type Booking struct { ID string `json:"_id" bson:"_id,omitempty"` - OccupiID string `json:"occupiId" bson:"occupiId,omitempty"` - RoomID string `json:"roomId" bson:"roomId" binding:"required"` + OccupiId string `json:"occupiId" bson:"occupiId,omitempty"` + RoomId string `json:"roomId" bson:"roomId" binding:"required"` RoomName string `json:"roomName" bson:"roomName" binding:"required"` Slot int `json:"slot" bson:"slot" binding:"required,min=1"` Emails []string `json:"emails" bson:"emails" binding:"required,dive,email"` @@ -32,9 +32,9 @@ type Booking struct { // structure of CheckIn type CheckIn struct { - BookingID string `json:"bookingId" bson:"bookingId"` - Creator string `json:"creator" bson:"creator"` - RoomID string `json:"roomId" bson:"roomId"` + BookingId string `json:"bookingId" bson:"bookingId binding:"required"` + Creator string `json:"creator" bson:"creator" binding:"required,email"` + RoomId string `json:"roomId" bson:"roomId" binding:"required"` } type OTP struct { @@ -50,7 +50,7 @@ type ViewBookings struct { type Room struct { ID string `json:"_id" bson:"_id,omitempty"` - RoomID string `json:"roomId" bson:"roomId,omitempty"` + RoomId string `json:"roomId" bson:"roomId,omitempty"` RoomNo int `json:"roomNo" bson:"roomNo,omitempty"` FloorNo int `json:"floorNo" bson:"floorNo,omitempty"` MinOccupancy int `json:"minOccupancy" bson:"minOccupancy,omitempty"` diff --git a/occupi-backend/pkg/utils/response.go b/occupi-backend/pkg/utils/response.go index 263940d5..5e27bc67 100644 --- a/occupi-backend/pkg/utils/response.go +++ b/occupi-backend/pkg/utils/response.go @@ -3,9 +3,8 @@ package utils import ( "net/http" - "github.com/gin-gonic/gin" - "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" + "github.com/gin-gonic/gin" ) // creates success response and formats it correctly From 4e37a6a6c2ad426c67ab8d5855a7b1a512bfef98 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 26 Jun 2024 21:22:01 +0200 Subject: [PATCH 10/54] chore: Add /benchmarks to .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b512c09d..8f7e2474 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -node_modules \ No newline at end of file +/benchmarks \ No newline at end of file From 67203b2f00cd9979af97aae5ab8c114eaabfacd6 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Wed, 26 Jun 2024 23:11:58 +0200 Subject: [PATCH 11/54] Linting errors should be fixed --- occupi-backend/pkg/database/database.go | 4 ++-- occupi-backend/pkg/handlers/api_handlers.go | 4 ++-- occupi-backend/pkg/models/database.go | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index 06584b83..24b38db3 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -129,8 +129,8 @@ func ConfirmCheckIn(ctx *gin.Context, db *mongo.Client, checkIn models.CheckIn) // Find the booking by bookingId, roomId, and creator filter := bson.M{ - "_id": checkIn.BookingId, - "roomId": checkIn.RoomId, + "_id": checkIn.BookingID, + "roomId": checkIn.RoomID, "creator": checkIn.Creator, } diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index c914681d..869523e1 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -62,7 +62,7 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { // Generate a unique ID for the booking booking.ID = primitive.NewObjectID().Hex() - booking.OccupiId = utils.GenerateBookingID() + booking.OccupiID = utils.GenerateBookingID() booking.CheckedIn = false // Save the booking to the database @@ -139,7 +139,7 @@ func CheckIn(ctx *gin.Context, appsession *models.AppSession) { } // Check if the booking exists - exists := database.BookingExists(ctx, appsession.DB, checkIn.BookingId) + exists := database.BookingExists(ctx, appsession.DB, checkIn.BookingID) if !exists { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to find booking", constants.InternalServerErrorCode, "Failed to find booking", nil)) return diff --git a/occupi-backend/pkg/models/database.go b/occupi-backend/pkg/models/database.go index ef6ae370..520c4610 100644 --- a/occupi-backend/pkg/models/database.go +++ b/occupi-backend/pkg/models/database.go @@ -17,7 +17,7 @@ type User struct { // structure of booking type Booking struct { ID string `json:"_id" bson:"_id,omitempty"` - OccupiId string `json:"occupiId" bson:"occupiId,omitempty"` + OccupiID string `json:"occupiId" bson:"occupiId,omitempty"` RoomId string `json:"roomId" bson:"roomId" binding:"required"` RoomName string `json:"roomName" bson:"roomName" binding:"required"` Slot int `json:"slot" bson:"slot" binding:"required,min=1"` @@ -32,9 +32,9 @@ type Booking struct { // structure of CheckIn type CheckIn struct { - BookingId string `json:"bookingId" bson:"bookingId binding:"required"` + BookingID string `json:"bookingId" bson:"bookingId" binding:"required"` Creator string `json:"creator" bson:"creator" binding:"required,email"` - RoomId string `json:"roomId" bson:"roomId" binding:"required"` + RoomID string `json:"roomId" bson:"roomId" binding:"required"` } type OTP struct { From 934bb3101ef0093596fc8805237529e58d4c3f1d Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Wed, 26 Jun 2024 23:17:03 +0200 Subject: [PATCH 12/54] All errors should be fixed --- occupi-backend/pkg/handlers/api_handlers.go | 2 +- occupi-backend/pkg/models/database.go | 2 +- occupi-backend/tests/utils_test.go | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index 869523e1..b4ab3822 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -122,7 +122,7 @@ func CancelBooking(ctx *gin.Context, appsession *models.AppSession) { } if err := mail.SendCancellationEmails(booking); err != nil { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "An error occured", constants.InternalServerErrorCode, "Failed to send booking email", nil)) + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "An error occurred", constants.InternalServerErrorCode, "Failed to send booking email", nil)) return } diff --git a/occupi-backend/pkg/models/database.go b/occupi-backend/pkg/models/database.go index 520c4610..8eb3e1ef 100644 --- a/occupi-backend/pkg/models/database.go +++ b/occupi-backend/pkg/models/database.go @@ -50,7 +50,7 @@ type ViewBookings struct { type Room struct { ID string `json:"_id" bson:"_id,omitempty"` - RoomId string `json:"roomId" bson:"roomId,omitempty"` + RoomID string `json:"roomId" bson:"roomId,omitempty"` RoomNo int `json:"roomNo" bson:"roomNo,omitempty"` FloorNo int `json:"floorNo" bson:"floorNo,omitempty"` MinOccupancy int `json:"minOccupancy" bson:"minOccupancy,omitempty"` diff --git a/occupi-backend/tests/utils_test.go b/occupi-backend/tests/utils_test.go index 6cc213ad..82a1a087 100644 --- a/occupi-backend/tests/utils_test.go +++ b/occupi-backend/tests/utils_test.go @@ -400,3 +400,23 @@ func TestGenerateOTP(t *testing.T) { t.Logf("Generated OTP: %s", otp) } +func TestLowercaseFirstLetter(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"Hello", "hello"}, + {"world", "world"}, + {"Golang", "golang"}, + {"", ""}, + {"A", "a"}, + {"ABC", "aBC"}, + } + + for _, test := range tests { + result := utils.LowercaseFirstLetter(test.input) + if result != test.expected { + t.Errorf("LowercaseFirstLetter(%q) = %q; expected %q", test.input, result, test.expected) + } + } +} From 457c141baf792ab2913659d90eeb6f4db4cb1783 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 26 Jun 2024 23:40:00 +0200 Subject: [PATCH 13/54] chore: Install mongosh for MongoDB integration testing --- .github/workflows/lint-test-build-golang.yml | 8 ++++++++ occupi-backend/pkg/database/seed.go | 16 +++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/lint-test-build-golang.yml b/.github/workflows/lint-test-build-golang.yml index 36304f45..83cfc5f9 100644 --- a/.github/workflows/lint-test-build-golang.yml +++ b/.github/workflows/lint-test-build-golang.yml @@ -59,6 +59,14 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Install mongosh + run: | + wget -qO - https://www.mongodb.org/static/pgp/server-5.0.asc | sudo apt-key add - + echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/5.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-5.0.list + sudo apt-get update + sudo apt-get install -y mongodb-mongosh + + - name: Wait for MongoDB to be ready run: | for i in {1..30}; do diff --git a/occupi-backend/pkg/database/seed.go b/occupi-backend/pkg/database/seed.go index a43a3162..910fa8e9 100644 --- a/occupi-backend/pkg/database/seed.go +++ b/occupi-backend/pkg/database/seed.go @@ -37,25 +37,25 @@ func SeedMockDatabase(mockdatafilepath string) { } // Insert data into each collection - var otpsDocuments []interface{} + otpsDocuments := make([]interface{}, 0, len(mockDatabase.OTPS)) for _, otps := range mockDatabase.OTPS { otpsDocuments = append(otpsDocuments, otps) } insertData(db.Database(configs.GetMongoDBName()).Collection("OTPS"), otpsDocuments) - var BookingsDocuments []interface{} + BookingsDocuments := make([]interface{}, 0, len(mockDatabase.Bookings)) for _, roomBooking := range mockDatabase.Bookings { BookingsDocuments = append(BookingsDocuments, roomBooking) } insertData(db.Database(configs.GetMongoDBName()).Collection("RoomBooking"), BookingsDocuments) - var roomsDocuments []interface{} + roomsDocuments := make([]interface{}, 0, len(mockDatabase.Rooms)) for _, rooms := range mockDatabase.Rooms { roomsDocuments = append(roomsDocuments, rooms) } insertData(db.Database(configs.GetMongoDBName()).Collection("Rooms"), roomsDocuments) - var usersDocuments []interface{} + usersDocuments := make([]interface{}, 0, len(mockDatabase.Users)) for _, users := range mockDatabase.Users { usersDocuments = append(usersDocuments, users) } @@ -70,15 +70,17 @@ func insertData(collection *mongo.Collection, documents []interface{}) { if err != nil { log.Fatalf("Failed to count documents: %v", err) } - if count == 0 && len(documents) > 0 { + + switch { + case count == 0 && len(documents) > 0: _, err := collection.InsertMany(context.Background(), documents) if err != nil { log.Fatalf("Failed to insert documents into %s collection: %v", collection.Name(), err) } log.Printf("Successfully seeded data into %s collection\n", collection.Name()) - } else if len(documents) == 0 { + case len(documents) == 0: log.Printf("No documents to insert into %s skipping seeding\n", collection.Name()) - } else { + default: log.Printf("Collection %s already has %d documents, skipping seeding\n", collection.Name(), count) } } From 3b9e172a5fb36f9e69d5f36bb1af5845c6ac0873 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 26 Jun 2024 23:46:55 +0200 Subject: [PATCH 14/54] chore: Create MongoDB user for testing with environment variables --- .github/workflows/lint-test-build-golang.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint-test-build-golang.yml b/.github/workflows/lint-test-build-golang.yml index 83cfc5f9..d34a2769 100644 --- a/.github/workflows/lint-test-build-golang.yml +++ b/.github/workflows/lint-test-build-golang.yml @@ -79,13 +79,17 @@ jobs: done - name: Create MongoDB User + env: + MONGO_INITDB_ROOT_USERNAME: ${{ secrets.MONGO_DB_TEST_USERNAME }} + MONGO_INITDB_ROOT_PASSWORD: ${{ secrets.MONGO_DB_TEST_PASSWORD }} + MONGO_INITDB_DATABASE: ${{ secrets.MONGO_DB_TEST_DB }} run: | mongosh ${{ secrets.MONGO_DB_TEST_URL }}/admin --eval ' db.createUser({ - user: ${{ secrets.MONGO_DB_TEST_USERNAME }}, - pwd: ${{ secrets.MONGO_DB_TEST_PASSWORD }}, + user: '${MONGO_INITDB_ROOT_USERNAME}', + pwd: '${MONGO_INITDB_ROOT_PASSWORD}', roles: [ - { role: "readWrite", db: ${{ secrets.MONGO_DB_TEST_DB }} } + { role: "readWrite", db: '${MONGO_INITDB_DATABASE}' } ] }); ' From 9310c2c42a695406a290b0de7d78264b928feab2 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 26 Jun 2024 23:51:44 +0200 Subject: [PATCH 15/54] chore: Update MongoDB connection string for testing environment --- .github/workflows/lint-test-build-golang.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint-test-build-golang.yml b/.github/workflows/lint-test-build-golang.yml index d34a2769..c0bf095f 100644 --- a/.github/workflows/lint-test-build-golang.yml +++ b/.github/workflows/lint-test-build-golang.yml @@ -84,7 +84,7 @@ jobs: MONGO_INITDB_ROOT_PASSWORD: ${{ secrets.MONGO_DB_TEST_PASSWORD }} MONGO_INITDB_DATABASE: ${{ secrets.MONGO_DB_TEST_DB }} run: | - mongosh ${{ secrets.MONGO_DB_TEST_URL }}/admin --eval ' + mongosh ${{ secrets.MONGO_DB_TEST_URL }}/admin --eval " db.createUser({ user: '${MONGO_INITDB_ROOT_USERNAME}', pwd: '${MONGO_INITDB_ROOT_PASSWORD}', @@ -92,7 +92,7 @@ jobs: { role: "readWrite", db: '${MONGO_INITDB_DATABASE}' } ] }); - ' + " - name: Set up Go uses: actions/setup-go@v5 From 0d467abd65603612fe1f0525cbc43f327a42f723 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 26 Jun 2024 23:53:57 +0200 Subject: [PATCH 16/54] chore: Update MongoDB connection string for testing environment by reconfiguring double qouted strings --- .github/workflows/lint-test-build-golang.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-test-build-golang.yml b/.github/workflows/lint-test-build-golang.yml index c0bf095f..d5e3683e 100644 --- a/.github/workflows/lint-test-build-golang.yml +++ b/.github/workflows/lint-test-build-golang.yml @@ -89,7 +89,7 @@ jobs: user: '${MONGO_INITDB_ROOT_USERNAME}', pwd: '${MONGO_INITDB_ROOT_PASSWORD}', roles: [ - { role: "readWrite", db: '${MONGO_INITDB_DATABASE}' } + { role: 'readWrite', db: '${MONGO_INITDB_DATABASE}' } ] }); " From 1ff966e153ceecb69eaec805a0327a39ec378996 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 27 Jun 2024 00:38:48 +0200 Subject: [PATCH 17/54] chore: Update occupi documentation theme configuration --- documentation/occupi-docs/pages/index.mdx | 104 +++++++++++---------- documentation/occupi-docs/theme.config.jsx | 13 ++- 2 files changed, 65 insertions(+), 52 deletions(-) diff --git a/documentation/occupi-docs/pages/index.mdx b/documentation/occupi-docs/pages/index.mdx index aaf91d57..a758e3ca 100644 --- a/documentation/occupi-docs/pages/index.mdx +++ b/documentation/occupi-docs/pages/index.mdx @@ -57,92 +57,94 @@ To get started, please feel free to explore the top navbar or the sidebar and na # Meet the team behind occupi - +
    + - - - - - - - - - - +
    - + + -

    Kamo

    - Project Manager, Fullstack Engineer
    - Kamo is a skilled software developer with a strong focus on database engineering and architecture. He is proficient in both front-end and back-end development, making him a well-rounded contributor to any team. -

    - +
    +

    Kamo

    +

    Project Manager, Fullstack Engineer


    +

    Kamo is a skilled software developer with a strong focus on database engineering and architecture. He is proficient in both front-end and back-end development, making him a well-rounded contributor to any team.

    +

    + - - + +
    - + + -

    Carey

    - Frontend Engineer, UI Engineer, Business Analyst, Testing Engineer
    - Carey has a solid understanding of business principles, business analysis and systems analysis with a strong support for technology-centered strategies that promote interactive learning and easier access to resources for independent study. -

    - +
    +

    Carey

    +

    Frontend Engineer, UI Engineer, Business Analyst, Testing Engineer


    +

    Carey has a solid understanding of business principles, business analysis and systems analysis with a strong support for technology-centered strategies that promote interactive learning and easier access to resources for independent study.

    +

    + - - + +
    - + + -

    Michael

    - Infrastructure management, Designer, Backend Engineer, DevOps, Frontend Engineer
    - Michael is a computer science enthusiast with a keen interest in creating engaging and aesthetically pleasing CS-related projects. His strengths lie in UI design, FullStack development, DevOps, and Systems Design, making him a versatile contributor to any team. -

    - +
    +

    Michael

    +

    Infrastructure management, Designer, Backend Engineer, DevOps, Frontend Engineer


    +

    Michael is a computer science enthusiast with a keen interest in creating engaging and aesthetically pleasing CS-related projects. His strengths lie in UI design, FullStack development, DevOps, and Systems Design, making him a versatile contributor to any team.

    +

    + - - + +
    - + + -

    Tinashe

    - Frontend Engineer, UI engineer, Business Analyst, Testing Engineer
    - Tinashe is a BSc Computer Science Major and Multimedia Minor student with a keen interest in UI/UX design and Human Computer Interaction. She loves learning and tackling new challenges. Her resilience makes her an asset in any team. -

    - +
    +

    Tinashe

    +

    Frontend Engineer, UI engineer, Business Analyst, Testing Engineer


    +

    Tinashe is a BSc Computer Science Major and Multimedia Minor student with a keen interest in UI/UX design and Human Computer Interaction. She loves learning and tackling new challenges. Her resilience makes her an asset in any team.

    +

    + - - + +
    - + + -

    Reta

    - Fullstack Engineer
    - Rethakgetse has a comprehensive skill set that spans both frontend and backend development, making him a well-rounded software developer. He is driven by a passion for problem-solving and a desire to make a tangible impact. He embraces the limitless possibilities offered by technology and is committed to leveraging his skills to develop transformative applications that address pressing industry needs, enhance operational efficiency, and improve customer satisfaction. -

    - +
    +

    Reta

    +

    Fullstack Engineer


    +

    Rethakgetse has a comprehensive skill set that spans both frontend and backend development, making him a well-rounded software developer. He is driven by a passion for problem-solving and a desire to make a tangible impact. He embraces the limitless possibilities offered by technology and is committed to leveraging his skills to develop transformative applications that address pressing industry needs, enhance operational efficiency, and improve customer satisfaction.

    +

    + - - + +
    ## Sponsors and Stakeholders diff --git a/documentation/occupi-docs/theme.config.jsx b/documentation/occupi-docs/theme.config.jsx index 6d377946..2d5b736f 100644 --- a/documentation/occupi-docs/theme.config.jsx +++ b/documentation/occupi-docs/theme.config.jsx @@ -6,6 +6,7 @@ export default { + Occupi ), logo: ( @@ -26,5 +27,15 @@ export default { link: 'https://discord.com', }, docsRepositoryBase: 'https://github.com/COS301-SE-2024/occupi/documentation/occupi-docs', - footerText: `MIT ${new Date().getFullYear()} © Occupi.`, + footer: { + text: ( + + MIT {new Date().getFullYear()} ©{' '} + + occupi + + . + + ) + } } \ No newline at end of file From f5f54df1e665d6a8e51633c327c5eb683d397319 Mon Sep 17 00:00:00 2001 From: Kamogelo Moeketse Date: Thu, 27 Jun 2024 02:13:52 +0200 Subject: [PATCH 18/54] Added loaders --- frontend/occupi-mobile4/.gitignore | 3 + frontend/occupi-mobile4/app.json | 16 ++++- frontend/occupi-mobile4/eas.json | 18 ++++++ .../screens/Booking/ViewBookingDetails.tsx | 58 ++++++++++++++----- 4 files changed, 78 insertions(+), 17 deletions(-) create mode 100644 frontend/occupi-mobile4/eas.json diff --git a/frontend/occupi-mobile4/.gitignore b/frontend/occupi-mobile4/.gitignore index 6623142a..d3e535a6 100644 --- a/frontend/occupi-mobile4/.gitignore +++ b/frontend/occupi-mobile4/.gitignore @@ -10,6 +10,9 @@ npm-debug.* *.orig.* web-build/ +eas.json +app.json + # macOS .DS_Store diff --git a/frontend/occupi-mobile4/app.json b/frontend/occupi-mobile4/app.json index 891d2e0d..2b11cd34 100644 --- a/frontend/occupi-mobile4/app.json +++ b/frontend/occupi-mobile4/app.json @@ -19,11 +19,15 @@ } }, "android": { - "permissions": ["USE_BIOMETRIC", "USE_FINGERPRINT"], + "permissions": [ + "USE_BIOMETRIC", + "USE_FINGERPRINT" + ], "adaptiveIcon": { "foregroundImage": "./assets/images/adaptive-icon.png", "backgroundColor": "#ffffff" - } + }, + "package": "com.y2kodedev.occupi" }, "web": { "bundler": "metro", @@ -35,6 +39,14 @@ ], "experiments": { "typedRoutes": true + }, + "extra": { + "router": { + "origin": false + }, + "eas": { + "projectId": "0127a6f2-e29a-4a67-a134-be8c4daed566" + } } } } diff --git a/frontend/occupi-mobile4/eas.json b/frontend/occupi-mobile4/eas.json new file mode 100644 index 00000000..bea8c5b7 --- /dev/null +++ b/frontend/occupi-mobile4/eas.json @@ -0,0 +1,18 @@ +{ + "cli": { + "version": ">= 10.0.2" + }, + "build": { + "development": { + "developmentClient": true, + "distribution": "internal" + }, + "preview": { + "distribution": "internal" + }, + "production": {} + }, + "submit": { + "production": {} + } +} diff --git a/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx b/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx index 5b489eed..f918f6b4 100644 --- a/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx +++ b/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx @@ -17,7 +17,7 @@ import { EvilIcons, MaterialIcons } from '@expo/vector-icons'; -import { useColorScheme, StyleSheet, TouchableOpacity } from 'react-native'; +import { useColorScheme, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native'; import { widthPercentageToDP as wp } from 'react-native-responsive-screen'; @@ -33,6 +33,7 @@ const ViewBookingDetails = (bookingId, roomName) => { const room = JSON.parse(roomData); const router = useRouter(); const [checkedIn, setCheckedIn] = useState(room.checkedIn); + const [isLoading, setIsLoading] = useState(false); const toast = useToast(); console.log("HERE:" + roomData); console.log(checkedIn); @@ -43,6 +44,7 @@ const ViewBookingDetails = (bookingId, roomName) => { "creator": room.creator, "roomId": room.roomId }; + setIsLoading(true); console.log(body); try { const response = await fetch('https://dev.occupi.tech/api/check-in', { @@ -64,19 +66,21 @@ const ViewBookingDetails = (bookingId, roomName) => { placement: 'top', render: ({ id }) => { return ( - + {data.message} ); }, }); + setIsLoading(false); } else { + setIsLoading(false); console.log(data); toast.show({ placement: 'top', render: ({ id }) => { return ( - + {data.message} ); @@ -93,6 +97,7 @@ const ViewBookingDetails = (bookingId, roomName) => { "_id": room._id, "creator": room.creator, }; + setIsLoading(true); console.log(body); try { const response = await fetch('https://dev.occupi.tech/api/cancel-booking', { @@ -113,20 +118,22 @@ const ViewBookingDetails = (bookingId, roomName) => { placement: 'top', render: ({ id }) => { return ( - + {data.message} ); }, }); + setIsLoading(false); router.push("/home"); } else { + setIsLoading(false); console.log(data); toast.show({ placement: 'top', render: ({ id }) => { return ( - + {data.message} ); @@ -184,25 +191,46 @@ const ViewBookingDetails = (bookingId, roomName) => { ViewBooking - {!checkedIn ? ( - checkin()}> + {isLoading ? ( + - Check in + ) : ( - checkin()}> + !checkedIn ? ( + checkin()}> + + + Check in + + + ) : ( + checkin()}> + + + Check out + + + ) + )} + + + {!isLoading ? ( + cancelBooking()}> - Check out + Delete Booking + + + ) : ( + + + )} - cancelBooking()}> - - Delete Booking - - + ) From 08c7cc669ec103f95bf9275913bdafd878124e37 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 27 Jun 2024 15:06:54 +0200 Subject: [PATCH 19/54] chore: Update Dockerfile to include environment flag for building the Go application --- occupi-backend/Dockerfile.dev | 5 +---- occupi-backend/Dockerfile.prod | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/occupi-backend/Dockerfile.dev b/occupi-backend/Dockerfile.dev index 6b9fa3f5..59667925 100644 --- a/occupi-backend/Dockerfile.dev +++ b/occupi-backend/Dockerfile.dev @@ -8,14 +8,11 @@ WORKDIR /app COPY go.mod go.sum ./ RUN go mod download && go mod verify -# copy .dev.env file -COPY .dev.env .env - # Copy the source code into the container COPY . . # Build the Go application -RUN go build -o occupi-backend ./cmd/occupi-backend +RUN go build -o occupi-backend ./cmd/occupi-backend -env=dev.deployed # Expose the port the app runs on EXPOSE 8081 diff --git a/occupi-backend/Dockerfile.prod b/occupi-backend/Dockerfile.prod index 4126be29..4aafefb2 100644 --- a/occupi-backend/Dockerfile.prod +++ b/occupi-backend/Dockerfile.prod @@ -8,14 +8,11 @@ WORKDIR /app COPY go.mod go.sum ./ RUN go mod download && go mod verify -# copy .prod.env file -COPY .prod.env .env - # Copy the source code into the container COPY . . # Build the Go application -RUN go build -o occupi-backend ./cmd/occupi-backend +RUN go build -o occupi-backend ./cmd/occupi-backend -env=prod # Expose the port the app runs on EXPOSE 8080 From ee6ac9aa25e12a4520b9267a33228633a38d45f2 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 27 Jun 2024 15:07:57 +0200 Subject: [PATCH 20/54] chore: Update actions to watch yaml config instead of env --- .github/workflows/deploy-golang-develop.yml | 10 +++++++--- .github/workflows/deploy-golang-prod.yml | 10 +++++++--- .github/workflows/lint-test-build-golang.yml | 8 ++++++-- occupi-backend/.dev.env.gpg | 2 -- occupi-backend/.env.gpg | Bin 602 -> 0 bytes occupi-backend/.prod.env.gpg | Bin 637 -> 0 bytes occupi-backend/.test.env.gpg | Bin 584 -> 0 bytes occupi-backend/cert.pem.gpg | Bin 1587 -> 0 bytes occupi-backend/configs/config.yaml.gpg | Bin 0 -> 268 bytes occupi-backend/configs/dev.deployed.yaml.gpg | 3 +++ occupi-backend/configs/prod.yaml.gpg | Bin 0 -> 651 bytes occupi-backend/configs/test.yaml.gpg | Bin 0 -> 591 bytes occupi-backend/key.pem.gpg | Bin 2587 -> 0 bytes 13 files changed, 23 insertions(+), 10 deletions(-) delete mode 100644 occupi-backend/.dev.env.gpg delete mode 100644 occupi-backend/.env.gpg delete mode 100644 occupi-backend/.prod.env.gpg delete mode 100644 occupi-backend/.test.env.gpg delete mode 100644 occupi-backend/cert.pem.gpg create mode 100644 occupi-backend/configs/config.yaml.gpg create mode 100644 occupi-backend/configs/dev.deployed.yaml.gpg create mode 100644 occupi-backend/configs/prod.yaml.gpg create mode 100644 occupi-backend/configs/test.yaml.gpg delete mode 100644 occupi-backend/key.pem.gpg diff --git a/.github/workflows/deploy-golang-develop.yml b/.github/workflows/deploy-golang-develop.yml index 9d555db1..cb5a0085 100644 --- a/.github/workflows/deploy-golang-develop.yml +++ b/.github/workflows/deploy-golang-develop.yml @@ -58,10 +58,14 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Decrypt env variables + - name: Decrypt defualt variables run: | - echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 .dev.env.gpg > .dev.env - + echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/config.yaml.gpg > configs/config.yaml + + - name: Decrypt test variables + run: | + echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/dev.deployed.yaml.gpg > configs/dev.deployed.yaml + - name: Build and push Docker image uses: docker/build-push-action@v5 with: diff --git a/.github/workflows/deploy-golang-prod.yml b/.github/workflows/deploy-golang-prod.yml index 325bfdab..543c0475 100644 --- a/.github/workflows/deploy-golang-prod.yml +++ b/.github/workflows/deploy-golang-prod.yml @@ -50,10 +50,14 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Decrypt env variables + - name: Decrypt defualt variables run: | - echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 .prod.env.gpg > .prod.env - + echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/config.yaml.gpg > configs/config.yaml + + - name: Decrypt test variables + run: | + echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/prod.yaml.gpg > configs/prod.yaml + - name: Build and push Docker image uses: docker/build-push-action@v5 with: diff --git a/.github/workflows/lint-test-build-golang.yml b/.github/workflows/lint-test-build-golang.yml index d5e3683e..327a1840 100644 --- a/.github/workflows/lint-test-build-golang.yml +++ b/.github/workflows/lint-test-build-golang.yml @@ -99,9 +99,13 @@ jobs: with: go-version: '1.21' # Specify the Go version you are using - - name: Decrypt env variables + - name: Decrypt defualt variables run: | - echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 .test.env.gpg > .test.env + echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/config.yaml.gpg > configs/config.yaml + + - name: Decrypt test variables + run: | + echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/test.yaml.gpg > configs/test.yaml - name: Run tests run: | diff --git a/occupi-backend/.dev.env.gpg b/occupi-backend/.dev.env.gpg deleted file mode 100644 index 2ceb8bc6..00000000 --- a/occupi-backend/.dev.env.gpg +++ /dev/null @@ -1,2 +0,0 @@ -Œ  ”U ÊvÊÿ:ÿÒéÂӠеŽ]¸çYEq ÉðØþ~L°ÖE>¨›§ÿŽ›´Z&éõäP_$ö—ºF/¡¨‰¬b?~ÞáÖ‚âE* ÷+ßQNq@‚¥ÞM1hZpð"êŠ_Ñœ•u8p‰4 žpÉAÐËt H $ÒÔ<²OvòÓÿÔìë»ysH0­e ±t‰×UÍHK²J½ùw›I¨‹¦l8…„Æ’¬ÔÎ…K¶ äªëìÓ?îã¯E‚¡ÕJ7¤”.yïôÛÄbƒRPÅš„þpÔrqÕ^än–g ÿv8 C÷U»AL"PÑ’ÚweRÿ ºR›¦J?ŠÞxÝrr5eo—e[ϱ`…ª~qÔ»DðŠ2#;DoÜ”—ÖR§ë,ßÏH1üõ~haŒ_Æö:ƒ¹û•ÝU‘Iì À¡k5ËÖ8™¦p9´s)âï² ¯uÜf¾ãF_ozdAl„š¤%,5žŠ&ºâL§LZ#æÜ9dö?81™Y…SáØŠé^Øìægip\E80RÕ’Ó1ÙhÚyVó)xƒ\ˆ¹£°!×~Îa9Ì®•ÅœV˜ÿÍçÞ#9;ùÆ“-rçÖ©™Ù&ŽZô.ö\ˆòø|©Xó |çP†#–c£ -ÇyåS{Éç[¼’¯©+<ã2žY–iþÇ¡jN=°\ÕëÉÞ膨û¶ÐhÞië2×Ä‚Bõt⫱®át}w~Ë7¶0»I5÷öÞþ1ìl{ePñ‰¤1¸à]oíÐf™õjkÏJÒŽm3‰vß~ \ No newline at end of file diff --git a/occupi-backend/.env.gpg b/occupi-backend/.env.gpg deleted file mode 100644 index 187f8513595b8605e2f2ecf8faed3c2222766cf7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 602 zcmV-g0;T@d?+FV3v;E2%!rZ9TkXv?lQ3$x`XArq`PxlxL*7~PkEdjc zIUs9^f|Hcpeu~Ju_?fgLmREqU2s$)=JP0O0RWGiqF*BUewy^j)mfPJoTq%U4z1 z+gjdSW5zI7t|C4we)#&8B4?BG{2ivdy`-r^UFi$AoMglRwJ}1W5!E4##o>z7A zCoX}lZ05<72GgRTP_&nO6b`q*8DuzP*^(}n*ygua2Fju42MFx*q8z8Q5i|LjN8d1J z;CCo?fEY>c)7Vv| zv<$Dm_UKbZ0j=1|*vGkPNac6L^vlHtD!P#H_x(#+UJ>Pas6r^W(sVgvUKZUoVDou#<+eG@V+Cc_+#5{7<$qUH||9 diff --git a/occupi-backend/.prod.env.gpg b/occupi-backend/.prod.env.gpg deleted file mode 100644 index fdcdf599d50becb53580b14005aa8e99dcf79354..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 637 zcmV-@0)qXF4Fm}T0`!|2H_5*EkpI%@0rWODCSx{Qt+1#@P6|CVETW%4% zy*0uk#9F+Ek>yV;2Si= zV*b1ZH>WptEn)4}bs|$Ow$0?BaMp0oBD_hG^WuUM%hV4e)wk*ogze|z)RDjVgA?!9 zfGp)28Z6I$`M%0rZBBH&N{Ye z7S!wWpdCy#>IBOY%X#b2>f<|04aU`xmh}_&;iFq-FwQ%Q5<*muE~#unpB0l z5AY!pi8GN>`yS}Jv$j2Dj<3wjY2cS<)V;a2GWfY>;t~C=J|&FDStT` zQ&J326^wigyS+8sZ==+oS8Pk#u2CO#0w^9}$!O>UJ)kfT*CGd#Gc)05;4t9fTYu_0 zc{LYlYB~7pUUQ?1&8CsVl^^>IO~|A-qxGm^%~EpWQT|MEX2RgI$z7-H%f(WsWt)1c zR^AQT!d~C=UR7E0YY~{1BJD&Fs5PTd{@>vun$tatLvb}k10r|og4|_CDz5oo zy#3O|qbTN=iz#}|MKjQTT#msB1hfK#$6%DGLcC<-UNsoxZsHWzh;xuE3*PpSX``bO X>?9VaXdH9gDISP+uj`#bHXbHH{d`6M diff --git a/occupi-backend/.test.env.gpg b/occupi-backend/.test.env.gpg deleted file mode 100644 index c4d97e702ba989222df807aaa4f2c7897d09bfee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 584 zcmV-O0=NB)4Fm}T0L~z|qy3S3jWfm? zr1{*gpN1nS)OcKc@f`MwJj18`&bz$Axh>!uT4_07V|ohrIRy|%RQ?96hKO2ZSB=TuGX%*Cc& ziv_;}48JOd9nXO{z`U5cE{;FJ-+FP8axUr72E8=$sh7=7(HQ*>`{$d+)-` zr|n5&y*3s%TcxzWTKha>1ja*+&c0NG08x_N()J zvp??W-Jr>KKua$^yk5r6{rhxf4tQG%Lg6GA+9EzD;sM42XSiH4KXzg%5Cu5v1s8ye4lb+! diff --git a/occupi-backend/cert.pem.gpg b/occupi-backend/cert.pem.gpg deleted file mode 100644 index 368c1b8325132264772b8e6e40a8cd76e9310320..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1587 zcmV-32F&@44Fm}T0x5GDhaF7kRR7ZI0m6*nLy{#dOqdq{62qeVZ?O#CdaGDZ0HPXA z2X(SqW4Oee6HZmTr&q^#DzBBJiiVw_OZ;k@`(b8iKMq)co2E1ZvUFCYGq;JRB|HM1 zMJ5G;AzS)IT&c7n@}@FCouim+!Q3?OVTtS@G$}EHS-NwENxRvB+Wo95j=8#RGz8UM2s%z*Ht!639m6EL5SiK^T*48IfKlQ= zSm=V7L?4`tHGSI6lECh+i;y%6UolJNn=<8pxKtA09y4S=8K2YznCQO{!Kdw=b@I7c zmQ1((m4Gkb73)7+TM~G_>t9NuXS${`qbZC6nVRbPY94@Ir6DREcERN*d109>W#32K9M9sB|tt_Qx!f9=f`7UWeK&u^vWG%#&{{}zlNX7g-8!#MSxYaA}sOuS7 zhtG4IU7XB+1z%8D+z91x3?gqLhzfUzcFhkXT@lVb_QOL~>H{v(nX;8oAwW<(b+0}| zn0!sNO6lh{;lelB{L5Bm$FSu04syelP`*2Q)&=8b%pU)S4-z25()WMd0dc~^L6ACz z>tc)Pvn0Y9b+4|MOpvUR?=^4vKAS91prywMm3mx|1HjQH_*$i6-%3Y(}Kr zRut2=1DoxNI=w}~1T?I+$IjH~kzTj@ zB8v}izb4>sah-z=u`QKnZio(-&rChLPu<{3>v?`}c=0t|s8oW6^U2WaYN{n%Cs_2L z?F3f)lFJ@RphGD{Elis2GOVyj=$`&uL0a6$WAh_`CY4Mu0Z|xxcTpm|OKZhLdRiz2 zJITZ{`EpT?>BB&>@eQy8v)$T+)wb8NsyAqB(Nf1RZ|>qAA%@z8)_?Zm#BC0^^aEU3 z1N&O&9maf(%>B=p-1aO-X#68e-P*vq*F zWkK}&^hK8aV6f2azDz}3GUI@w2jmU~@Oma6$~4|Iq}fC|BoAktY}6AE2xOBOgfl}h7ke3kfu0a%@2`l%H)q$1}(Svi3WO5o6b;2-i(i3Rbq+R9g7(N z)GEzcitp{vg^7?m_%$deKPtvgtoy1RZ zu|HGDTu4v#q|VY+pIRN@njPoDut5(C{wviO0%-sT6S-se><;a@PnSNhfhe|UhL+YR z6J<(XkBbY?p$dXA`z|I+agjL+A?-Jzk$u~&26PDj6QKaWTlbg&`@fq>-czfL=Nd lIv}ujV#>|-lNsHTKEExxVU|+l$fEwZYDha}gxuu%)?asj6g&U` diff --git a/occupi-backend/configs/config.yaml.gpg b/occupi-backend/configs/config.yaml.gpg new file mode 100644 index 0000000000000000000000000000000000000000..dad021a41aa8deace2621374c96b8e868f050a4e GIT binary patch literal 268 zcmV+n0rUQh4Fm}T0?S@;6Rl-IKk?GQIsu76qnfY+7fSICrG zD5+Ri*tH|fHX@3)Z3b~dM6jyP_h~DrqeA{f+*tknzQt+~hRi}~x(!20310AC+PM4t zpzo;M*KeD@H?(C~4l&jucm>M9*FQ~7AXTW}fM5*c3J^QU5ID~Drx;5Novf~ZwQyACG5*D# zt3(b;YMh*XJbrJd9~}oA_wxYjS1tL|f35Pi7nsbpf2%ugTEu!vleEB|(#trdIG%m# S@x;>`pnsjWLP$Z77pGe<6NIY( literal 0 HcmV?d00001 diff --git a/occupi-backend/configs/dev.deployed.yaml.gpg b/occupi-backend/configs/dev.deployed.yaml.gpg new file mode 100644 index 00000000..0acf116f --- /dev/null +++ b/occupi-backend/configs/dev.deployed.yaml.gpg @@ -0,0 +1,3 @@ +Œ  昅 +÷”~ñÒéD~A¥¿1Ùú**]Ä–É€G%ð\Ð%<Æ“ºäøüw…²*„ÎÛÏM çÙÀAÅ7Ó/©{°Ó] Õü~àÈLÛaÊB‰‹ '‡ÊéUÃÄpà~°ÀOûß>âïžx@ÑfêhêòG40¥ ¸(CËxi»ÚµiôUÂæ‹•Ìÿ‚Ñðʆ@ýÎûÿáË |ýˆ;6%q±„“VA²äý½ÊH’܇1Ôúå<‚Àö8ãM¸çƒ!u_öš¥À§e û³³¨›R>€¿š.%óÛ…ZKÏëPÏcÕ7Z_šÔ6«*65²êýÓ¹ñ¿oA$EC\šß{y^ ~Ç:¸}Xr3ü×Ô;L†L/zƒë£´<¨ëßHSVê-qýÿóþ*<…Ö;Â1tþðÜv;ÍÔ,6Û©rŠÊ×K;5>½Ë|ôj7[?v`p?—/&þû " N¥(’`«ªï'i꼡¼€M•©3…Ç´¿}‘ü>³²:œ +õœ%+ {CÄ4*(xØóºA´ôãFà&{gm!£ξŠV}’Å›ù Ì©kù4L^Êžäb§­á7Â.2åÇó•Ü&º{¹Û þ–»¤NÉä€3›ÜÝêhŒ³_ËbïÈuŸjÕ>Ÿ#Ôâ²Á{6*Cy“ 0¡kr\‚+ -·Aèl|ÿ/C×Xµ'HÒ_çY0‡ûš¸#Úþ•”P (Ù_áŸNÒGŠa½Ôp¶<ÙñC€<ÕÌw­×Õ CÞF§j R™ÆcæB †;'¯‰;ùÛ¨[SšÏJ~¨µXgr¿ì±'–J º¸*!6ÎÆgôhb-ƒ<™t} \ No newline at end of file diff --git a/occupi-backend/configs/prod.yaml.gpg b/occupi-backend/configs/prod.yaml.gpg new file mode 100644 index 0000000000000000000000000000000000000000..28fb0392680e30253260007377efca56c9030fa0 GIT binary patch literal 651 zcmV;60(AY14Fm}T0*040o{A0}7xB{R0nO$YG(OX}i^Yb)!RJo{m^VM=g)7+j4(cYl z#=O*+YWQLypz6bw7Kvq@(e>=MB?RA|6QY@VgNf%8MAw4|aVGt;0G|FMppxD~972FrH^T>l|Ru7Q?P?6I*5KT8cT}8suMX(k0n>H@O|7gQySYMCGZC-95 z-pocqWbx@;V)Os>`5m|t`bhI*(W-`(c)ifuaXMPMCpamk#+)H12xFU3Qn7@Pa|54c zmb=0Vir2JX!*)Ok4#N#j>;}PCPdJISGgaE18Tbf+)`tuGbtuN@lwH3MO+MX{=I|Jo zc0SkNs6d%}CwG)(E#~`UylozIw@^!ExAQnM431mS$Rg1;ssEARZbS=D zO7z7Xj_=?Aecr>pn{xiR3xK|V*#evw>V2=Obb5R|Bowmpkr z%98(LP_pE+rU84k8*mUn=h+RTOQRL~f2;%8kR)!jj46;In|o!DcIb{C|Ka?4&Ni(o zw}hBcDme(x4{VM764dDYcN4dP lt^eM5OpoZPg~Vx3^(Pe^Fwvi;&8(TbW+6{B->aU&*DZWJO&kCK literal 0 HcmV?d00001 diff --git a/occupi-backend/configs/test.yaml.gpg b/occupi-backend/configs/test.yaml.gpg new file mode 100644 index 0000000000000000000000000000000000000000..77ee64d384fa777952bd11d53291f4ec29ff04a1 GIT binary patch literal 591 zcmV-V0w`LeEL-sm48;*+1WS0)Z=#ipS z#vT##*&0LteTEIq#$6+I%ybSg<3oEzdalgplNtsv%AEWHPQ<6Hez#|PRT8@#4EOR} zxzINnK|Nr(U1=RQm}@K2a}2&lPohc!CZv@WPIS*DJtaEK-Qy#JG{rvD1Uxl0-D{L! zVJqzoYFEh(li}tt!08^K{{mS2)g7_)7X*j%lWkb`a{28Gd%Bg{si{8jj?TONB=|zF z0SsnR-bG_c$Yns`@5&JAQ1F6`fhFAaX^z9{{*G^Q5Cqxv?;z5h(DUw}!%V&u&fuk> z^bWs7o1ZU8o>gbogNt-xlBbs(e5PDr$SB3(nob}%c#N@(*l)Nu{87$ZoR6jmh1WQ9 z!o9;c_I}j72W5wS85rlQT69jF%U<|W z*N&RN=o)~NVp*864??sP<$MFoLx-F^;mYy#*6749~}2ZFBH%+Db{W5j!4_s^z%H#Td+ z-zHdG4L?4P#R*9{G1xwIavkviQ;G@X%l6iL{mZU{(o)e1!mcsukMnd@Q({Yg<_Nmx z!#2nES`;%%i0>x_uz7vMJ*j=(mX=Jx0rS@yL$A0mh?ScnU2rt@PD|-4&IM dvfeW{w5DN%{80VvbH?_XQENK7$ literal 0 HcmV?d00001 diff --git a/occupi-backend/key.pem.gpg b/occupi-backend/key.pem.gpg deleted file mode 100644 index 60947e2de938118d5bc9f2528ca3ac6d0174397b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2587 zcmV+$3gq>S4Fm}T0s`$gn-}4J=l{~{0YoE~)@Z=1PBfhou9rLq5+XKmT>;*l%HQgJ zGCTsBD3&j#fCC`}FXaU!B3YhhSGwyHMjs=R2Dpi)l?e#;&`$AZ6pifz+Vigwa|G6R z@}sV-E|bg1_89s%-}71gvSd5?MUKOuuOa6az(OHz1df{Txg<+erf&+8FYC7;FDz62 z%DCq@=98Xvz{Mh{@qyaa9drExe9!VlOy^k=x^n-mpODWw!8?O~H-J_vky|DO#Xi`i(5YK%Tvo=M5nE91a>L)Ux!m8U^*#WBw&FJ^AX@#q|RqT)pTma zu!-a2(?Bi8?u4(eucZa&h_!xnSEfY0BD6 zuFzpcglOnx+%a;Nc-vms@wN|JO1cEZeN7`f|)JudvAcx9Fawt`Ntlxymtsf*` z^m7$A`EL&T+uGNZl>$jWC5ct>Xc@|i64xo%=t4=;#Qjr0dUcK4wVn6WW{1nH*QESQ zOirnnXNN}m<5(W52zYabH+jxUYAJ_VA=brVb%$BTdC~S2dUMXQHeMqAKo6hjWYlWW z8QgXz6PcU4W_M2LDVjlK{GfISmctXO@Z~P)p0@5Q1T(YAWXk?ll2!rkt%0DroS(6fKRsRJ zEkIve#FT>bjK*34b!-ahSG1lk@xq8O zb0oUVB~4y2hT3WGB@2FBJ9eC^E!6}Q-W6`S8d8YbNk?VtjgJqZ_Uy0@r#0`X8`v_V z%Rgssd%824;`4I0E)V4Zf|V8-_cz&3xL}a#Pn`9BFr{N9P!HlV$y6yx7VxdwOr=*A< zzY;Ccnd{x+A6Hj86#;HD^495oZ%Z<$XS1H_O`As8_`j6HyimuPv!Rl&RWPhp$m)Pj5GsFIUS_?Kq{#SkY;V=Hn6w?OHmosQqqvUz(|`(s3;mhT|6{bF(b1bRh-n7zt@~Ni#FOwvi&{RTWrIXaH>qhygu@eFGjkK4oJ3?T~gx?+R2iOdu*=!jJf)&grrE zZc4rfy0z4C_P(&>xCK^S%31qHPAm=TD-kA&0DI-Dzd1bYIsRkh(5N`h4{!248iQaO z=L5)TD81wjWsYM{G5RXTLdwh1G4b76>OhXBOk-u%T@Ghi2*p^znx_TjsVXL<+p}03 zck2)}zM(pM)>O$<(Iy2d3&>k>gMbJ<<^3lE}dP*^SvC2 z5;ekV72A&#V;V4G+qap}shvHH2Ym6Avl{~s5V2Ld|9xF|=Kx_v971a4z;f0FB84o- zwl@!-axLnP1o{VjOeWfuX^4vZOQ_L>LGZYFSd`{-x_g~V&~U|`^4+m!MJA&~ZlM+Q zG*bejzQdv_5!kxm=r~%~_cV-EHcd`@Vmqtkm~V>}J3XAfIrd$quH6hqE(`f-VSPeA zaaFcEd95vtx~=a=Z7;8nNMl-}D&1A5 zZ}Xp8Nu0P0!5mbiHYe4aN|HHD8NF;g?H+=O>h6jjj+Ni^?H`)=e$n(4nfvP(#b-!q zoYiRQ$p6=WyFwVwhdzY0GP9=C!77ZXn4(7rk_`YA-(sb~KVHJ98FM2vP!8$L-k=cO z+CB0OiQT?AnRGzm{jB+10?@U5Os;4ad?GG&Ou8h(Qlo`W&NGiN`XScVA=9HDV*l3e ziICBF8U0b*#1%n^@IIEC@hcrfO`Ie8`c27~Y_mw3Gd`gCZUmBunG`#*C(7y$tjYy7 ze&^cvQIdA$;%kV-4UNhM@6Wn9QXq?(wT8@@lIXU@@EXWBCUB6oB#lTY={2jO)1#T0 z-1=7EJyYee#~OT8@2S=3ij{kVPbSNl>|ghC*R&5JmB>a4U{KYn{HJ?c!)SG|uk;e4 zWzYwp8lP2+wNXTsZos)vn_j(8ot;pE7pb^}vCqXojL*9k6Is6(QhTx~ON4dSKD|;T z-Elzu`tAR{L>LGu8ShkKOlQ_79XF42yB$uvfYQ^E>v0zQkkB;Np!A!V7&C*yS5*MB zj%>+6o1rEvS%G281p9z`H!4+S`=t56#Xb0K>5LFh^o1$^jFI5s4Z^;OIwl#l1;3VR z9P~6rjh@=`3oh}R-_<5ne((;g2hhE5uOJZo6%v;!_oA)_D3jq1TDvb|+f5KRjXW~M zg*l@wYw0pCU^a4xlXL~GYnH9#*aq@@$dszDlqaNj^7~#Kx**3)?7hiZZBX$61-@ek z@mS{ooMQAgQF;lP+3~i3c+X%<;ZTlzH@Bkuu2`G}IuGD`UpbEWh9XhX#U{877h`Wy zv@|qy)VTnz%O>*c+GVb8$e;a7+xmRYy&$+5h{*A0gSnHlNi}4EsWy&u!s>rtzG3=Z zNrN!na11A|c%ocAS;cxXPK1lisYt>yRnlbxXylx2NL{W0Zb7^JtpgbTT82i>##{Jt x?kt^)Hzc|!1dfuU3no9o|MDXzd1$g&CR_8&KZACC@EW|VA_%CH6miJY@eMEX9K8Sl From 67f719d22004c704306b48f57bcd152b16396a1d Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 27 Jun 2024 15:09:12 +0200 Subject: [PATCH 21/54] chore: added gitignore for secret yaml files --- occupi-backend/configs/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 occupi-backend/configs/.gitignore diff --git a/occupi-backend/configs/.gitignore b/occupi-backend/configs/.gitignore new file mode 100644 index 00000000..2a616051 --- /dev/null +++ b/occupi-backend/configs/.gitignore @@ -0,0 +1 @@ +*.yaml \ No newline at end of file From 90e3ee55be13ebbd00fa5e7f2f659451c36b0626 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 27 Jun 2024 15:10:31 +0200 Subject: [PATCH 22/54] refactor: migrated from using plain .env files to using viper managed yaml files to facilitate and make development easier for test, dev-local, dev-deployed and prod environments --- occupi-backend/cmd/occupi-backend/main.go | 31 ++++---- occupi-backend/configs/config.go | 90 ++++++++++++++++++----- occupi-backend/go.mod | 22 +++++- occupi-backend/go.sum | 38 ++++++++++ occupi-backend/occupi.bat | 16 ++-- occupi-backend/occupi.sh | 16 ++-- occupi-backend/pkg/constants/constants.go | 21 +++--- occupi-backend/tests/main_test.go | 9 +-- 8 files changed, 174 insertions(+), 69 deletions(-) diff --git a/occupi-backend/cmd/occupi-backend/main.go b/occupi-backend/cmd/occupi-backend/main.go index a154db55..6267660d 100644 --- a/occupi-backend/cmd/occupi-backend/main.go +++ b/occupi-backend/cmd/occupi-backend/main.go @@ -1,11 +1,9 @@ package main import ( - "fmt" - "log" + "flag" "github.com/gin-gonic/gin" - "github.com/joho/godotenv" "github.com/sirupsen/logrus" "github.com/COS301-SE-2024/occupi/occupi-backend/configs" @@ -17,10 +15,12 @@ import ( // occupi backend entry point func main() { - // Load environment variables from .env file - if err := godotenv.Load(); err != nil { - log.Fatal(fmt.Printf("Error loading .env file with error as %s", err)) - } + // Define the environment flag + env := flag.String("env", "dev.localhost", "Environment to use (dev.localhost, dev.deployed, prod)") + flag.Parse() + + // init viper + configs.InitViper(env) // setup logger to log all server interactions utils.SetupLogger() @@ -49,19 +49,20 @@ func main() { certFile := configs.GetCertFileName() keyFile := configs.GetKeyFileName() - // fatal error if the cert or key file is not found - if certFile == "CERTIFICATE_FILE_PATH" || keyFile == "KEY_FILE_PATH" { - logrus.Fatal("Cert or Key file not found") - } - // logrus all env variables logrus.Infof("Server running on port: %s", configs.GetPort()) logrus.Infof("Server running in %s mode", configs.GetGinRunMode()) logrus.Infof("Server running with cert file: %s", certFile) logrus.Infof("Server running with key file: %s", keyFile) - // Listening on the port with TLS - if err := ginRouter.RunTLS(":"+configs.GetPort(), certFile, keyFile); err != nil { - logrus.Fatal("Failed to run server: ", err) + // Listening on the port with TLS if env is prod or dev.deployed + if configs.GetEnv() == "prod" || configs.GetEnv() == "dev.deployed" { + if err := ginRouter.RunTLS(":"+configs.GetPort(), certFile, keyFile); err != nil { + logrus.Fatal("Failed to run server: ", err) + } + } else { + if err := ginRouter.Run(":" + configs.GetPort()); err != nil { + logrus.Fatal("Failed to run server: ", err) + } } } diff --git a/occupi-backend/configs/config.go b/occupi-backend/configs/config.go index b9392cf6..fd26374a 100644 --- a/occupi-backend/configs/config.go +++ b/occupi-backend/configs/config.go @@ -1,16 +1,58 @@ package configs import ( - "os" + "log" "strconv" "strings" + + "github.com/spf13/viper" +) + +const ( + MONGODB_USERNAME = "MONGODB_USERNAME" + MONGODB_PASSWORD = "MONGODB_PASSWORD" + MONGODB_CLUSTERURI = "MONGODB_CLUSTERURI" + MONGODB_DBNAME = "MONGODB_DBNAME" + MONGODB_START_URI = "MONGODB_START_URI" + PORT = "PORT" + LOG_FILE_NAME = "LOG_FILE_NAME" + SMTP_HOST = "SMTP_HOST" + SMTP_PORT = "SMTP_PORT" + SMTP_PASSWORD = "SMTP_PASSWORD" + SYSTEM_EMAIL = "SYSTEM_EMAIL" + CERTIFICATE_FILE_PATH = "CERTIFICATE_FILE_PATH" + KEY_FILE_PATH = "KEY_FILE_PATH" + GIN_RUN_MODE = "GIN_RUN_MODE" + TRUSTED_PROXIES = "TRUSTED_PROXIES" + JWT_SECRET = "JWT_SECRET" + SESSION_SECRET = "SESSION_SECRET" + OCCUPI_DOMAINS = "OCCUPI_DOMAINS" + ENV = "ENV" ) -// define configs in this file +// init viper +func InitViper(envtype *string, configpath ...string) { + viper.SetConfigName("config") + viper.SetConfigType("yaml") + if len(configpath) > 0 { + viper.AddConfigPath(configpath[0]) + } else { + viper.AddConfigPath("./configs") + } + if err := viper.ReadInConfig(); err != nil { + log.Fatalf("Error reading config file: %s", err) + } + + // Merge environment-specific config + viper.SetConfigName(*envtype) + if err := viper.MergeInConfig(); err != nil { + log.Fatalf("Error merging config file: %s", err) + } +} // gets the port to start the server on as defined in the .env file func GetPort() string { - port := os.Getenv("PORT") + port := viper.GetString(PORT) if port == "" { port = "PORT" } @@ -19,7 +61,7 @@ func GetPort() string { // gets the mongodb username as defined in the .env file func GetMongoDBUsername() string { - username := os.Getenv("MONGODB_USERNAME") + username := viper.GetString(MONGODB_USERNAME) if username == "" { username = "MONGODB_USERNAME" } @@ -28,7 +70,7 @@ func GetMongoDBUsername() string { // gets the mongodb password as defined in the .env file func GetMongoDBPassword() string { - password := os.Getenv("MONGODB_PASSWORD") + password := viper.GetString(MONGODB_PASSWORD) if password == "" { password = "MONGODB_PASSWORD" } @@ -37,7 +79,7 @@ func GetMongoDBPassword() string { // gets the mongodb cluster uri as defined in the .env file func GetMongoDBCLUSTERURI() string { - uri := os.Getenv("MONGODB_CLUSTERURI") + uri := viper.GetString(MONGODB_CLUSTERURI) if uri == "" { uri = "MONGODB_CLUSTERURI" } @@ -46,7 +88,7 @@ func GetMongoDBCLUSTERURI() string { // gets the mongodb name as defined in the .env file func GetMongoDBName() string { - name := os.Getenv("MONGODB_DBNAME") + name := viper.GetString(MONGODB_DBNAME) if name == "" { name = "MONGODB_DBNAME" } @@ -55,7 +97,7 @@ func GetMongoDBName() string { // gets the mongodb start uri as defined in the .env file func GetMongoDBStartURI() string { - startURI := os.Getenv("MONGODB_START_URI") + startURI := viper.GetString(MONGODB_START_URI) if startURI == "" { startURI = "MONGODB_START_URI" } @@ -63,7 +105,7 @@ func GetMongoDBStartURI() string { } func GetLogFileName() string { - logFileName := os.Getenv("LOG_FILE_NAME") + logFileName := viper.GetString(LOG_FILE_NAME) if logFileName == "" { logFileName = "LOG_FILE_NAME" } @@ -72,7 +114,7 @@ func GetLogFileName() string { // gets the system email as defined in the .env file func GetSystemEmail() string { - email := os.Getenv("SYSTEM_EMAIL") + email := viper.GetString(SYSTEM_EMAIL) if email == "" { email = "" } @@ -82,7 +124,7 @@ func GetSystemEmail() string { // GetSMTPPort retrieves the SMTP port from the environment and converts it to an integer. // If the environment variable is not set, it returns the default port 587. func GetSMTPPort() int { - port := os.Getenv("SMTP_PORT") + port := viper.GetString(SMTP_PORT) if port == "" { return 587 } @@ -97,7 +139,7 @@ func GetSMTPPort() int { // gets the smtp password as defined in the .env file func GetSMTPPassword() string { - password := os.Getenv("SMTP_PASSWORD") + password := viper.GetString(SMTP_PASSWORD) if password == "" { password = "" } @@ -106,7 +148,7 @@ func GetSMTPPassword() string { // gets the smtp host as defined in the .env file func GetSMTPHost() string { - host := os.Getenv("SMTP_HOST") + host := viper.GetString(SMTP_HOST) if host == "" { host = "smtp.gmail.com" } @@ -115,7 +157,7 @@ func GetSMTPHost() string { // gets the certificate file name as defined in the .env file func GetCertFileName() string { - certFileName := os.Getenv("CERTIFICATE_FILE_PATH") + certFileName := viper.GetString(CERTIFICATE_FILE_PATH) if certFileName == "" { certFileName = "CERTIFICATE_FILE_PATH" } @@ -124,7 +166,7 @@ func GetCertFileName() string { // gets the key file name as defined in the .env file func GetKeyFileName() string { - keyFileName := os.Getenv("KEY_FILE_PATH") + keyFileName := viper.GetString(KEY_FILE_PATH) if keyFileName == "" { keyFileName = "KEY_FILE_PATH" } @@ -133,7 +175,7 @@ func GetKeyFileName() string { // gets gins run mode as defined in the .env file func GetGinRunMode() string { - ginRunMode := os.Getenv("GIN_RUN_MODE") + ginRunMode := viper.GetString(GIN_RUN_MODE) if ginRunMode == "" { ginRunMode = "debug" } @@ -142,7 +184,7 @@ func GetGinRunMode() string { // gets list of trusted proxies as defined in the .env file func GetTrustedProxies() []string { - trustedProxies := os.Getenv("TRUSTED_PROXIES") + trustedProxies := viper.GetString(TRUSTED_PROXIES) if trustedProxies != "" { proxyList := strings.Split(trustedProxies, ",") return proxyList @@ -151,7 +193,7 @@ func GetTrustedProxies() []string { } func GetJWTSecret() string { - secret := os.Getenv("JWT_SECRET") + secret := viper.GetString(JWT_SECRET) if secret == "" { secret = "JWT_SECRET" } @@ -159,7 +201,7 @@ func GetJWTSecret() string { } func GetSessionSecret() string { - secret := os.Getenv("SESSION_SECRET") + secret := viper.GetString(SESSION_SECRET) if secret == "" { secret = "SESSION_SECRET" } @@ -167,10 +209,18 @@ func GetSessionSecret() string { } func GetOccupiDomains() []string { - domains := os.Getenv("OCCUPI_DOMAINS") + domains := viper.GetString(OCCUPI_DOMAINS) if domains != "" { domainList := strings.Split(domains, ",") return domainList } return []string{""} } + +func GetEnv() string { + env := viper.GetString(ENV) + if env == "" { + env = "ENV" + } + return env +} diff --git a/occupi-backend/go.mod b/occupi-backend/go.mod index 025af0b8..29140271 100644 --- a/occupi-backend/go.mod +++ b/occupi-backend/go.mod @@ -25,8 +25,9 @@ require ( github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.4 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect @@ -40,27 +41,41 @@ require ( github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.2.2 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.7 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/kr/pretty v0.3.0 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.19.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.24.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect @@ -68,5 +83,6 @@ require ( google.golang.org/protobuf v1.34.1 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/occupi-backend/go.sum b/occupi-backend/go.sum index c6d6d94c..6f30fc67 100644 --- a/occupi-backend/go.sum +++ b/occupi-backend/go.sum @@ -16,8 +16,12 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI= @@ -53,6 +57,8 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -67,16 +73,21 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -86,15 +97,32 @@ github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8 github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -107,6 +135,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= @@ -124,6 +154,10 @@ github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= @@ -133,6 +167,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -193,6 +229,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/occupi-backend/occupi.bat b/occupi-backend/occupi.bat index 18c02689..7f8ba870 100644 --- a/occupi-backend/occupi.bat +++ b/occupi-backend/occupi.bat @@ -2,16 +2,16 @@ setlocal if "%1 %2" == "run dev" ( - go run -v cmd/occupi-backend/main.go + go run -v cmd/occupi-backend/main.go -env=dev.localhost exit /b 0 ) else if "%1 %2" == "run prod" ( - go run cmd/occupi-backend/main.go + go run cmd/occupi-backend/main.go -env=dev.localhost exit /b 0 ) else if "%1 %2" == "build dev" ( - go build -v cmd/occupi-backend/main.go + go build -v cmd/occupi-backend/main.go -env=dev.localhost exit /b 0 ) else if "%1 %2" == "build prod" ( - go build cmd/occupi-backend/main.go + go build cmd/occupi-backend/main.go -env=dev.localhost exit /b 0 ) else if "%1" == "test" ( go test -v ./tests/... @@ -35,10 +35,10 @@ if "%1 %2" == "run dev" ( echo Usage: occupi.bat {command} echo. echo Available commands: -echo run dev : go run -v cmd/occupi-backend/main.go -echo run prod : go run cmd/occupi-backend/main.go -echo build dev : go build -v cmd/occupi-backend/main.go -echo build prod : go build cmd/occupi-backend/main.go +echo run dev : go run -v cmd/occupi-backend/main.go -env=dev.localhost +echo run prod : go run cmd/occupi-backend/main.go -env=dev.localhost +echo build dev : go build -v cmd/occupi-backend/main.go -env=dev.localhost +echo build prod : go build cmd/occupi-backend/main.go -env=dev.localhost echo test : go test ./tests/... echo test codecov : go test ./tests/... -race -coverprofile=coverage.out -covermode=atomic echo lint : golangci-lint run diff --git a/occupi-backend/occupi.sh b/occupi-backend/occupi.sh index 2bacff9a..f9ebbd53 100644 --- a/occupi-backend/occupi.sh +++ b/occupi-backend/occupi.sh @@ -4,10 +4,10 @@ print_help() { echo "Usage: $0 {command}" echo "" echo "Available commands:" - echo " run dev -> go run -v cmd/occupi-backend/main.go" - echo " run prod -> go run cmd/occupi-backend/main.go" - echo " build dev -> go build -v cmd/occupi-backend/main.go" - echo " build prod -> go build cmd/occupi-backend/main.go" + echo " run dev -> go run -v cmd/occupi-backend/main.go -env=dev.localhost" + echo " run prod -> go run cmd/occupi-backend/main.go -env=dev.localhost" + echo " build dev -> go build -v cmd/occupi-backend/main.go -env=dev.localhost" + echo " build prod -> go build cmd/occupi-backend/main.go -env=dev.localhost" echo " test -> go test ./tests/..." echo " test codecov -> go test ./tests/... -race -coverprofile=coverage.out -covermode=atomic" echo " lint -> golangci-lint run" @@ -15,13 +15,13 @@ print_help() { } if [ "$1" = "run" ] && [ "$2" = "dev" ]; then - go run -v cmd/occupi-backend/main.go + go run -v cmd/occupi-backend/main.go -env=dev.localhost elif [ "$1" = "run" ] && [ "$2" = "prod" ]; then - go run cmd/occupi-backend/main.go + go run cmd/occupi-backend/main.go -env=dev.localhost elif [ "$1" = "build" ] && [ "$2" = "dev" ]; then - go build -v cmd/occupi-backend/main.go + go build -v cmd/occupi-backend/main.go -env=dev.localhost elif [ "$1" = "build" ] && [ "$2" = "prod" ]; then - go build cmd/occupi-backend/main.go + go build cmd/occupi-backend/main.go -env=dev.localhost elif [ "$1" = "test" ]; then go test -v ./tests/... elif [ "$1" = "test" ] && [ "$2" = "codecov" ]; then diff --git a/occupi-backend/pkg/constants/constants.go b/occupi-backend/pkg/constants/constants.go index 599815a1..a3241054 100644 --- a/occupi-backend/pkg/constants/constants.go +++ b/occupi-backend/pkg/constants/constants.go @@ -1,12 +1,13 @@ package constants -const InvalidRequestPayloadCode = "INVALID_REQUEST_PAYLOAD" -const BadRequestCode = "BAD_REQUEST" -const InvalidAuthCode = "INVALID_AUTH" -const IncompleteAuthCode = "INCOMPLETE_AUTH" -const InternalServerErrorCode = "INTERNAL_SERVER_ERROR" -const UnAuthorizedCode = "UNAUTHORIZED" -const Admin = "admin" -const Basic = "basic" - -const AdminDBAccessOption = "authSource=admin" +const ( + InvalidRequestPayloadCode = "INVALID_REQUEST_PAYLOAD" + BadRequestCode = "BAD_REQUEST" + InvalidAuthCode = "INVALID_AUTH" + IncompleteAuthCode = "INCOMPLETE_AUTH" + InternalServerErrorCode = "INTERNAL_SERVER_ERROR" + UnAuthorizedCode = "UNAUTHORIZED" + Admin = "admin" + Basic = "basic" + AdminDBAccessOption = "authSource=admin" +) diff --git a/occupi-backend/tests/main_test.go b/occupi-backend/tests/main_test.go index b9bcd101..648001b4 100644 --- a/occupi-backend/tests/main_test.go +++ b/occupi-backend/tests/main_test.go @@ -5,19 +5,18 @@ import ( "os" "testing" + "github.com/COS301-SE-2024/occupi/occupi-backend/configs" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils" - "github.com/joho/godotenv" ) func TestMain(m *testing.M) { log.Println("Configuring test env") - // Load environment variables from .env file - if err := godotenv.Load("../.test.env"); err != nil { - log.Fatal("Error loading .env file: ", err) - } + // init viper + env := "test" + configs.InitViper(&env, "../configs") // setup logger to log all server interactions utils.SetupLogger() From f7a613848d459544379b24e15408a84b7f572ccc Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 27 Jun 2024 15:11:59 +0200 Subject: [PATCH 23/54] chore: Fix typo in decrypt default variables step --- .github/workflows/deploy-golang-develop.yml | 2 +- .github/workflows/deploy-golang-prod.yml | 2 +- .github/workflows/lint-test-build-golang.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-golang-develop.yml b/.github/workflows/deploy-golang-develop.yml index cb5a0085..94cc9459 100644 --- a/.github/workflows/deploy-golang-develop.yml +++ b/.github/workflows/deploy-golang-develop.yml @@ -58,7 +58,7 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Decrypt defualt variables + - name: Decrypt default variables run: | echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/config.yaml.gpg > configs/config.yaml diff --git a/.github/workflows/deploy-golang-prod.yml b/.github/workflows/deploy-golang-prod.yml index 543c0475..f3a5c685 100644 --- a/.github/workflows/deploy-golang-prod.yml +++ b/.github/workflows/deploy-golang-prod.yml @@ -50,7 +50,7 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Decrypt defualt variables + - name: Decrypt default variables run: | echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/config.yaml.gpg > configs/config.yaml diff --git a/.github/workflows/lint-test-build-golang.yml b/.github/workflows/lint-test-build-golang.yml index 327a1840..bb04c963 100644 --- a/.github/workflows/lint-test-build-golang.yml +++ b/.github/workflows/lint-test-build-golang.yml @@ -99,7 +99,7 @@ jobs: with: go-version: '1.21' # Specify the Go version you are using - - name: Decrypt defualt variables + - name: Decrypt default variables run: | echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/config.yaml.gpg > configs/config.yaml From 1619cb562261e197048cc2a2dbb0a322e2442bfb Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 27 Jun 2024 15:35:20 +0200 Subject: [PATCH 24/54] refactor: Migrate from using plain .env files to using viper managed yaml files for easier development and environment management --- occupi-backend/configs/config.go | 107 ++++++++++++++++--------------- 1 file changed, 56 insertions(+), 51 deletions(-) diff --git a/occupi-backend/configs/config.go b/occupi-backend/configs/config.go index fd26374a..75b41c9e 100644 --- a/occupi-backend/configs/config.go +++ b/occupi-backend/configs/config.go @@ -9,25 +9,25 @@ import ( ) const ( - MONGODB_USERNAME = "MONGODB_USERNAME" - MONGODB_PASSWORD = "MONGODB_PASSWORD" - MONGODB_CLUSTERURI = "MONGODB_CLUSTERURI" - MONGODB_DBNAME = "MONGODB_DBNAME" - MONGODB_START_URI = "MONGODB_START_URI" - PORT = "PORT" - LOG_FILE_NAME = "LOG_FILE_NAME" - SMTP_HOST = "SMTP_HOST" - SMTP_PORT = "SMTP_PORT" - SMTP_PASSWORD = "SMTP_PASSWORD" - SYSTEM_EMAIL = "SYSTEM_EMAIL" - CERTIFICATE_FILE_PATH = "CERTIFICATE_FILE_PATH" - KEY_FILE_PATH = "KEY_FILE_PATH" - GIN_RUN_MODE = "GIN_RUN_MODE" - TRUSTED_PROXIES = "TRUSTED_PROXIES" - JWT_SECRET = "JWT_SECRET" - SESSION_SECRET = "SESSION_SECRET" - OCCUPI_DOMAINS = "OCCUPI_DOMAINS" - ENV = "ENV" + MongodbUsername = "MONGODB_USERNAME" + MongodbPassword = "MONGODB_PASSWORD" + MongodbClusteruri = "MONGODB_CLUSTERURI" + MongodbDbname = "MONGODB_DBNAME" + MongodbStartUri = "MONGODB_START_URI" + Port = "PORT" + LogFileName = "LOG_FILE_NAME" + SmtpHost = "SMTP_HOST" + SmtpPort = "SMTP_PORT" + SmtpPassword = "SMTP_PASSWORD" + SystemEmail = "SYSTEM_EMAIL" + CertificateFilePath = "CERTIFICATE_FILE_PATH" + KeyFilePath = "KEY_FILE_PATH" + GinRunMode = "GIN_RUN_MODE" + TrustedProxies = "TRUSTED_PROXIES" + JwtSecret = "JWT_SECRET" + SessionSecret = "SESSION_SECRET" + OccupiDomains = "OCCUPI_DOMAINS" + Env = "ENV" ) // init viper @@ -50,71 +50,72 @@ func InitViper(envtype *string, configpath ...string) { } } -// gets the port to start the server on as defined in the .env file +// gets the port to start the server on as defined in the config.yaml file func GetPort() string { - port := viper.GetString(PORT) + port := viper.GetString(Port) if port == "" { port = "PORT" } return port } -// gets the mongodb username as defined in the .env file +// gets the mongodb username as defined in the config.yaml file func GetMongoDBUsername() string { - username := viper.GetString(MONGODB_USERNAME) + username := viper.GetString(MongodbUsername) if username == "" { username = "MONGODB_USERNAME" } return username } -// gets the mongodb password as defined in the .env file +// gets the mongodb password as defined in the config.yaml file func GetMongoDBPassword() string { - password := viper.GetString(MONGODB_PASSWORD) + password := viper.GetString(MongodbPassword) if password == "" { password = "MONGODB_PASSWORD" } return password } -// gets the mongodb cluster uri as defined in the .env file +// gets the mongodb cluster uri as defined in the config.yaml file func GetMongoDBCLUSTERURI() string { - uri := viper.GetString(MONGODB_CLUSTERURI) + uri := viper.GetString(MongodbClusteruri) if uri == "" { uri = "MONGODB_CLUSTERURI" } return uri } -// gets the mongodb name as defined in the .env file +// gets the mongodb name as defined in the config.yaml file func GetMongoDBName() string { - name := viper.GetString(MONGODB_DBNAME) + name := viper.GetString(MongodbDbname) if name == "" { name = "MONGODB_DBNAME" } return name } -// gets the mongodb start uri as defined in the .env file +// gets the mongodb start uri as defined in the config.yaml file func GetMongoDBStartURI() string { - startURI := viper.GetString(MONGODB_START_URI) + startURI := viper.GetString(MongodbStartUri) if startURI == "" { startURI = "MONGODB_START_URI" } return startURI } +// gets the log file name as defined in the config.yaml file func GetLogFileName() string { - logFileName := viper.GetString(LOG_FILE_NAME) + logFileName := viper.GetString(LogFileName) if logFileName == "" { logFileName = "LOG_FILE_NAME" } return logFileName } -// gets the system email as defined in the .env file +// gets the system email as defined in the config.yaml file func GetSystemEmail() string { - email := viper.GetString(SYSTEM_EMAIL) + email := viper.GetString(SystemEmail) if email == "" { email = "" } @@ -124,7 +125,7 @@ func GetSystemEmail() string { // GetSMTPPort retrieves the SMTP port from the environment and converts it to an integer. // If the environment variable is not set, it returns the default port 587. func GetSMTPPort() int { - port := viper.GetString(SMTP_PORT) + port := viper.GetString(SmtpPort) if port == "" { return 587 } @@ -137,54 +138,54 @@ func GetSMTPPort() int { return portInt } -// gets the smtp password as defined in the .env file +// gets the smtp password as defined in the config.yaml file func GetSMTPPassword() string { - password := viper.GetString(SMTP_PASSWORD) + password := viper.GetString(SmtpPassword) if password == "" { password = "" } return password } -// gets the smtp host as defined in the .env file +// gets the smtp host as defined in the config.yaml file func GetSMTPHost() string { - host := viper.GetString(SMTP_HOST) + host := viper.GetString(SmtpHost) if host == "" { host = "smtp.gmail.com" } return host } -// gets the certificate file name as defined in the .env file +// gets the certificate file name as defined in the config.yaml file func GetCertFileName() string { - certFileName := viper.GetString(CERTIFICATE_FILE_PATH) + certFileName := viper.GetString(CertificateFilePath) if certFileName == "" { certFileName = "CERTIFICATE_FILE_PATH" } return certFileName } -// gets the key file name as defined in the .env file +// gets the key file name as defined in the config.yaml file func GetKeyFileName() string { - keyFileName := viper.GetString(KEY_FILE_PATH) + keyFileName := viper.GetString(KeyFilePath) if keyFileName == "" { keyFileName = "KEY_FILE_PATH" } return keyFileName } -// gets gins run mode as defined in the .env file +// gets gins run mode as defined in the config.yaml file func GetGinRunMode() string { - ginRunMode := viper.GetString(GIN_RUN_MODE) + ginRunMode := viper.GetString(GinRunMode) if ginRunMode == "" { ginRunMode = "debug" } return ginRunMode } -// gets list of trusted proxies as defined in the .env file +// gets list of trusted proxies as defined in the config.yaml file func GetTrustedProxies() []string { - trustedProxies := viper.GetString(TRUSTED_PROXIES) + trustedProxies := viper.GetString(TrustedProxies) if trustedProxies != "" { proxyList := strings.Split(trustedProxies, ",") return proxyList @@ -192,24 +193,27 @@ func GetTrustedProxies() []string { return []string{""} } +// gets the JWT secret as defined in the config.yaml file func GetJWTSecret() string { - secret := viper.GetString(JWT_SECRET) + secret := viper.GetString(JwtSecret) if secret == "" { secret = "JWT_SECRET" } return secret } +// gets the session secret as defined in the config.yaml file func GetSessionSecret() string { - secret := viper.GetString(SESSION_SECRET) + secret := viper.GetString(SessionSecret) if secret == "" { secret = "SESSION_SECRET" } return secret } +// gets the list of occupi domains as defined in the config.yaml file func GetOccupiDomains() []string { - domains := viper.GetString(OCCUPI_DOMAINS) + domains := viper.GetString(OccupiDomains) if domains != "" { domainList := strings.Split(domains, ",") return domainList @@ -217,8 +221,9 @@ func GetOccupiDomains() []string { return []string{""} } +// gets the environment type as defined in the config.yaml file func GetEnv() string { - env := viper.GetString(ENV) + env := viper.GetString(Env) if env == "" { env = "ENV" } From ace60cb1a0574539e74ecfd77896aa0440e6d220 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 27 Jun 2024 15:39:16 +0200 Subject: [PATCH 25/54] chore: Update SMTP configuration variables to use uppercase naming convention --- occupi-backend/configs/config.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/occupi-backend/configs/config.go b/occupi-backend/configs/config.go index 75b41c9e..2dbbe102 100644 --- a/occupi-backend/configs/config.go +++ b/occupi-backend/configs/config.go @@ -13,12 +13,12 @@ const ( MongodbPassword = "MONGODB_PASSWORD" MongodbClusteruri = "MONGODB_CLUSTERURI" MongodbDbname = "MONGODB_DBNAME" - MongodbStartUri = "MONGODB_START_URI" + MongodbStartURI = "MONGODB_START_URI" Port = "PORT" LogFileName = "LOG_FILE_NAME" - SmtpHost = "SMTP_HOST" - SmtpPort = "SMTP_PORT" - SmtpPassword = "SMTP_PASSWORD" + SMTPHost = "SMTP_HOST" + SMTPPort = "SMTP_PORT" + SMTPPassword = "SMTP_PASSWORD" SystemEmail = "SYSTEM_EMAIL" CertificateFilePath = "CERTIFICATE_FILE_PATH" KeyFilePath = "KEY_FILE_PATH" @@ -97,7 +97,7 @@ func GetMongoDBName() string { // gets the mongodb start uri as defined in the config.yaml file func GetMongoDBStartURI() string { - startURI := viper.GetString(MongodbStartUri) + startURI := viper.GetString(MongodbStartURI) if startURI == "" { startURI = "MONGODB_START_URI" } @@ -125,7 +125,7 @@ func GetSystemEmail() string { // GetSMTPPort retrieves the SMTP port from the environment and converts it to an integer. // If the environment variable is not set, it returns the default port 587. func GetSMTPPort() int { - port := viper.GetString(SmtpPort) + port := viper.GetString(SMTPPort) if port == "" { return 587 } @@ -140,7 +140,7 @@ func GetSMTPPort() int { // gets the smtp password as defined in the config.yaml file func GetSMTPPassword() string { - password := viper.GetString(SmtpPassword) + password := viper.GetString(SMTPPassword) if password == "" { password = "" } @@ -149,7 +149,7 @@ func GetSMTPPassword() string { // gets the smtp host as defined in the config.yaml file func GetSMTPHost() string { - host := viper.GetString(SmtpHost) + host := viper.GetString(SMTPHost) if host == "" { host = "smtp.gmail.com" } From 13443093b9b85fa26996de1356095459bbb1c275 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 27 Jun 2024 15:49:31 +0200 Subject: [PATCH 26/54] chore: Update Dockerfile to include environment flag for building the Go application --- occupi-backend/Dockerfile.dev | 4 ++-- occupi-backend/Dockerfile.prod | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/occupi-backend/Dockerfile.dev b/occupi-backend/Dockerfile.dev index 59667925..e8983a25 100644 --- a/occupi-backend/Dockerfile.dev +++ b/occupi-backend/Dockerfile.dev @@ -12,10 +12,10 @@ RUN go mod download && go mod verify COPY . . # Build the Go application -RUN go build -o occupi-backend ./cmd/occupi-backend -env=dev.deployed +RUN go build -o occupi-backend ./cmd/occupi-backend # Expose the port the app runs on EXPOSE 8081 # Command to run the executable -CMD ["./occupi-backend"] \ No newline at end of file +CMD ["./occupi-backend", "-env=dev.deployed"] \ No newline at end of file diff --git a/occupi-backend/Dockerfile.prod b/occupi-backend/Dockerfile.prod index 4aafefb2..fd8f838f 100644 --- a/occupi-backend/Dockerfile.prod +++ b/occupi-backend/Dockerfile.prod @@ -12,10 +12,10 @@ RUN go mod download && go mod verify COPY . . # Build the Go application -RUN go build -o occupi-backend ./cmd/occupi-backend -env=prod +RUN go build -o occupi-backend ./cmd/occupi-backend # Expose the port the app runs on EXPOSE 8080 # Command to run the executable -CMD ["./occupi-backend"] \ No newline at end of file +CMD ["./occupi-backend", "-env=prod"] \ No newline at end of file From 1d45b2767c2fa1655c3e01a3f58aee9beba234c2 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 27 Jun 2024 16:29:04 +0200 Subject: [PATCH 27/54] chore: Update environment variable check for devdeployed in main.go --- occupi-backend/cmd/occupi-backend/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/occupi-backend/cmd/occupi-backend/main.go b/occupi-backend/cmd/occupi-backend/main.go index 6267660d..d3b96a4e 100644 --- a/occupi-backend/cmd/occupi-backend/main.go +++ b/occupi-backend/cmd/occupi-backend/main.go @@ -56,7 +56,7 @@ func main() { logrus.Infof("Server running with key file: %s", keyFile) // Listening on the port with TLS if env is prod or dev.deployed - if configs.GetEnv() == "prod" || configs.GetEnv() == "dev.deployed" { + if configs.GetEnv() == "prod" || configs.GetEnv() == "devdeployed" { if err := ginRouter.RunTLS(":"+configs.GetPort(), certFile, keyFile); err != nil { logrus.Fatal("Failed to run server: ", err) } From ddffb98cd53b2152f94ba70b8416b75bbfc286cd Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 27 Jun 2024 22:55:02 +0200 Subject: [PATCH 28/54] chore: Update cookie expiration time to use time.Until() instead of int(expirationTime.Unix()) --- occupi-backend/configs/config.yaml.gpg | Bin 268 -> 271 bytes occupi-backend/configs/dev.deployed.yaml.gpg | Bin 660 -> 697 bytes occupi-backend/configs/dev.localhost.yaml.gpg | Bin 0 -> 656 bytes occupi-backend/configs/prod.yaml.gpg | Bin 651 -> 689 bytes occupi-backend/configs/test.yaml.gpg | Bin 591 -> 630 bytes occupi-backend/pkg/handlers/auth_handlers.go | 2 +- 6 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 occupi-backend/configs/dev.localhost.yaml.gpg diff --git a/occupi-backend/configs/config.yaml.gpg b/occupi-backend/configs/config.yaml.gpg index dad021a41aa8deace2621374c96b8e868f050a4e..92257cc4ac2d43364cc4a3597d006f2ef6bde8e6 100644 GIT binary patch literal 271 zcmV+q0r38e4Fm}T0*XiB&B`yABmdICJppRA&(&Owxb2@li04lwpWPz6_QtFC9aIg$ zt-+vhhStejA&FdC0zy7x!BuOx|GBRR;`DWEq8|sOZ%~RUL7!%OH-fo(6vo3(T{rYD z2&p9^tQWCnra{ycXPE0=QbsLlL4_ICDqyqSJa4qGSFfJUJHM!$)i{`#@GGUHkoIx8 z5!AqJ-I2X!-WM56mX}w?_yIZLmt2YXx3C-6HV~N}+hkaucibTnY1};9?iw@A5dwdy z8O8z*R7sjlg5i|A=M>b7O>(b9z6Z==__hDa-{Lj+6Q6~2!lE<$2D46cHmWX6L&e8L Vf^oD{YcD8F$D;x0&_q?J2C~qng~I>< literal 268 zcmV+n0rUQh4Fm}T0?S@;6Rl-IKk?GQIsu76qnfY+7fSICrG zD5+Ri*tH|fHX@3)Z3b~dM6jyP_h~DrqeA{f+*tknzQt+~hRi}~x(!20310AC+PM4t zpzo;M*KeD@H?(C~4l&jucm>M9*FQ~7AXTW}fM5*c3J^QU5ID~Drx;5Novf~ZwQyACG5*D# zt3(b;YMh*XJbrJd9~}oA_wxYjS1tL|f35Pi7nsbpf2%ugTEu!vleEB|(#trdIG%m# S@x;>`pnsjWLP$Z77pGe<6NIY( diff --git a/occupi-backend/configs/dev.deployed.yaml.gpg b/occupi-backend/configs/dev.deployed.yaml.gpg index 0acf116f63ef69e149e842cd827b16257c374f0b..2e60679a75e5a158677172638a49add11835ced8 100644 GIT binary patch literal 697 zcmV;q0!ICe4Fm}T0`2tLEZI!>R{zrJ0Z?}7jkIQS?^C_Zc#Y~C3>}mg;Z7PA#$IEi zl6Wzk7w7TYxQIR0PTPuF%ezlCkKAo7TY&lh*mzn8q74ukdKr9;8%DoJbAbMSnPqlH z1?38x%Gb|stK@%9SIP(DR6#}e7062o75}(0>RATfL3c$o#s*SJLub;bo|#jJ+4!gN zm^6&aAoM=B%x51za;B+3K zo3XkBYfzypz~ULm$#x9P%Rrmc@=k*y1+un(M0|ThC>p1XJPjZ=`ZcqMETGfcTD$-= zT&w!V42QM)=d&&hKwT4#errZjoq;0*7o^jk*CyY)F(;dtkL*tI4gjh1 zDB--9$vh>egkeZ}U?0lZWMv=%EC<+;IG7Q)W`X9*tsrCSCAV0Fk89I1q3Z>tR%q-U ztzi?50_oEsWOVDhi6tZO?^*)dpe*PU6%e_H*cxyr(pydJ*O#Ampaa=t!&gZ+rQuesxdLc536*Z>u*ZJ+z5@R zNkrz^QpVnxi=uAD3;fgXRCRKbpjRf&lc~7)*t~6D{5JDqT*}B?*zy(gK5Rhq#XsGb zQBQEt+u-ZM-Arb^p7+P#vY13kIM8e3d$F^;$!$5L3G;6cVK8Q>LEGR?90yhB@nzwj fNdarbN-Xl;C*joFw)w%Z+C&|q8c3|NeNmnAp$}o9 literal 660 zcmV;F0&D$@4Fm}T0_K>73ipqce(}=j0YrX5rN5m96fxQQDk@#XmdSueCGcF(4<$Ut zle&K#awLB%)IFR6R5odeSWT?^Ixe&7elOxt0~LJx_H3@3-m z=^s_Y#Bkt#u)t6I-#+5+o_IjfX6k6_@<%iQFr}clC_~G5X}j9BY4jUa!sd&W%>RPX z@XCfj{mzh``~Ts~3w-^EJ2n(0aj}GxRzb4l{k_UalH7+e8r1sbJc7XXI2q$jxaWf* zbzkrl^Q)i)MeUz*e(Hmeyb0*y8` zvg!x@)4B1#Z$TI&MMGSg-+Osp4SvTuxP4f1GyEFY)H_UuOfMdKgX^QTJg6D#-$+wd z1nMnu{r~g+8!9}7);q#6bpG(%b{{*<)GRjJsd9?S*GoG!J{7&oeDrEJ2wOjPU~oT| z4lgGD`w1c+piZSIl3=SEs_!Rh>bwY{yns!WsSq=T$F#qFk^DXevz-~TI-CmioFyxu zdqcy7G%6@~*z>wUwDjXf;3j)#Z6Tus&c2FPeUinS`3uacYxy)xUdo>2j}Bs|t>HJq zE;8lE^Of8tx_i0X4gLa_6T75N$rt1gfHRvM+}-MEjI$nJ%VO`y7DSultDnz$Sz+WwW4 zPzfm6U*Vrl(npG6z0`2FJlXL>fIQXAcdggephMn9r)mpQnZ{%0LJWo#J14ILi97k* us9RH-&q{s1ki4PEC>`a}zzIUuBiNeTI_{ltJq z;!#YHmI!~$a%$)*tVb{O>m2-!Dv$!noil8t=fgHhUIy50UZznAOBb88X>;s>k#0P;Z+0MfiLiO z&Y#EU_Rtxc?N*?;dl_oHdPIkWrVdA6yQv!bf@VJRU8i+zr%lRu;wUK zaIjLxc3YWf@W7a5Qg1fDK3oA5C~u*;cKNh*5j;bP&3IQT|%q z^7F8S*x}qgc6UW(A|-Yy!RBZ&{a?Xs?iEUp(uC4r>H=~mEm{mYiM+q-D~&_Km<6Nj6I{IVR8axg{KxPo1eeJY=3? z?DuvSxAx@LCxp-J@VDbDNEJlTW!-bNOqL?L=q* literal 0 HcmV?d00001 diff --git a/occupi-backend/configs/prod.yaml.gpg b/occupi-backend/configs/prod.yaml.gpg index 28fb0392680e30253260007377efca56c9030fa0..37b368289a9a5f75054e944ee23fd8e0cd60ac18 100644 GIT binary patch literal 689 zcmV;i0#5ym4Fm}T0@mJr0Rn603AsPXhdHeo1hf#ls*8alA?H(tY8k(y zQg+@PgiiM}d3Wa(g15V$&0?3E8_t##@2i+V=67+K+Z++9y9xfMECqshQ;5 z7-MBHFb>jNRzJ=}YOlG-c2{>I_Ep-!i&xtI&R%1uPf8S9W(G>pvRlwf)prPqwrO!b z;keFdC3oQ!AXxaTHaG>nFvkEOsS{5ctE$s~^rJgFx+AqZLrL^=E$gR+*PCi77s4K*_u=h2n+##}S0zL#wCc{>qL#T6or`p!&jwpS1f~&fuoTgi>={3^{%-X^aWmL4D&D zKAAUx5)IRo!h+RQNmY3)^YtVdU}{Z`s5RF&H96hj)bf)Xv2^Av;%!E({c(9q$B4uz z*1j9@BrJFpfgamR2C$OO{y6?Dtv4M^?6VYr^1qCg{KAyvz0}A&+h=gTFn_U)G{cLJ z@R{qW`SP{D|3m2x;Y}xP{y<3adajBP-!2-^1M`a|7RzWq*Jdl}ik3OhC|dVMEoE45 z!pRZ})9*Y?#QbeitRXv%pK#1f=d>ru7?%%CSF{etI=N2U#%SVXazaMU4 zbsr*>Bv-~XqmC;Fat-9tYIm9p(rkr$9$@WU>fYS%p&CLmrA!?r`+GqhM1@9yd40XJ zjUY3~m1s31ZeMMJ6%rrz1NzhvhZ9=NEVYlQj!`+WE#rb}^^u96_=MB?RA|6QY@VgNf%8MAw4|aVGt;0G|FMppxD~972FrH^T>l|Ru7Q?P?6I*5KT8cT}8suMX(k0n>H@O|7gQySYMCGZC-95 z-pocqWbx@;V)Os>`5m|t`bhI*(W-`(c)ifuaXMPMCpamk#+)H12xFU3Qn7@Pa|54c zmb=0Vir2JX!*)Ok4#N#j>;}PCPdJISGgaE18Tbf+)`tuGbtuN@lwH3MO+MX{=I|Jo zc0SkNs6d%}CwG)(E#~`UylozIw@^!ExAQnM431mS$Rg1;ssEARZbS=D zO7z7Xj_=?Aecr>pn{xiR3xK|V*#evw>V2=Obb5R|Bowmpkr z%98(LP_pE+rU84k8*mUn=h+RTOQRL~f2;%8kR)!jj46;In|o!DcIb{C|Ka?4&Ni(o zw}hBcDme(x4{VM764dDYcN4dP lt^eM5OpoZPg~Vx3^(Pe^Fwvi;&8(TbW+6{B->aU&*DZWJO&kCK diff --git a/occupi-backend/configs/test.yaml.gpg b/occupi-backend/configs/test.yaml.gpg index 77ee64d384fa777952bd11d53291f4ec29ff04a1..e9596583b003ec8d0903a3fb8402c337a2fdf7fc 100644 GIT binary patch literal 630 zcmV-+0*U>M4Fm}T0y&e-J2O5oYyZ;e0jfAK8GQjU<$?q{BTokUqRq6JL2;@2;fczj zv;qX5Vbu!mQzl3p>ct7~poY=z?Qwl##Xf@MB0%@kjDq6^pu>GZxFiW}@kI;pansiN z_o&Rid)uF>8&lGWe7vp^mHS~T7)~uhRpg8mu;gAolQ0=dLzQzHNW*Q0ZaLL-UDsr@ z0_z2zPh~f4<7$eeo#w14-ja>rq{I85t1giDATqW3z}$aed&=(~HTNh&eNV+cx1wxi zbrb3e-bWMnlIgS|yP zV-H{E($Y*z6uq55v^TAbs3J9Gq<)QLczvsb5U(sHR16kQVWxQ&rggZPo5w;JElR0f zY#PHovul}%>ln7IfY{k_^yoQ`^>UPf?2ac38pLLwH3|Nsj#djTanemcq*=TVnil;* z6q;leyJS>iPQfJM-{#+IG(7kJwe|S!?{OAotCOD#VEPi@lcc8a^ahdSl>(E@EGqXS{3OGB#&bODlugG?uG-A(2 Qwk1ncgxR7s?EnA( literal 591 zcmV-V0w`LeEL-sm48;*+1WS0)Z=#ipS z#vT##*&0LteTEIq#$6+I%ybSg<3oEzdalgplNtsv%AEWHPQ<6Hez#|PRT8@#4EOR} zxzINnK|Nr(U1=RQm}@K2a}2&lPohc!CZv@WPIS*DJtaEK-Qy#JG{rvD1Uxl0-D{L! zVJqzoYFEh(li}tt!08^K{{mS2)g7_)7X*j%lWkb`a{28Gd%Bg{si{8jj?TONB=|zF z0SsnR-bG_c$Yns`@5&JAQ1F6`fhFAaX^z9{{*G^Q5Cqxv?;z5h(DUw}!%V&u&fuk> z^bWs7o1ZU8o>gbogNt-xlBbs(e5PDr$SB3(nob}%c#N@(*l)Nu{87$ZoR6jmh1WQ9 z!o9;c_I}j72W5wS85rlQT69jF%U<|W z*N&RN=o)~NVp*864??sP<$MFoLx-F^;mYy#*6749~}2ZFBH%+Db{W5j!4_s^z%H#Td+ z-zHdG4L?4P#R*9{G1xwIavkviQ;G@X%l6iL{mZU{(o)e1!mcsukMnd@Q({Yg<_Nmx z!#2nES`;%%i0>x_uz7vMJ*j=(mX=Jx0rS@yL$A0mh?ScnU2rt@PD|-4&IM dvfeW{w5DN%{80VvbH?_XQENK7$ diff --git a/occupi-backend/pkg/handlers/auth_handlers.go b/occupi-backend/pkg/handlers/auth_handlers.go index 766785d8..e4c33dcd 100644 --- a/occupi-backend/pkg/handlers/auth_handlers.go +++ b/occupi-backend/pkg/handlers/auth_handlers.go @@ -172,7 +172,7 @@ func Login(ctx *gin.Context, appsession *models.AppSession, role string) { logrus.Error(err) return } - ctx.SetCookie("token", token, int(expirationTime.Unix()), "/", "", false, true) + ctx.SetCookie("token", token, int(time.Until(expirationTime).Seconds()), "/", "", false, true) ctx.JSON(http.StatusOK, utils.SuccessResponse( http.StatusOK, From d39e5ca4e39aea8d8d5a869498462856c79beae3 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Fri, 28 Jun 2024 02:18:25 +0200 Subject: [PATCH 29/54] Validation has changed a lot need to update docs heavy --- occupi-backend/pkg/database/database.go | 19 ++---- occupi-backend/pkg/handlers/api_handlers.go | 69 +++++++++++++-------- occupi-backend/pkg/mail/mail.go | 20 +++--- occupi-backend/pkg/models/database.go | 36 +++++++---- occupi-backend/pkg/utils/utils.go | 49 +++++++++++++++ occupi-backend/tests/handlers_test.go | 41 +++++------- occupi-backend/tests/utils_test.go | 32 ++++++++++ 7 files changed, 183 insertions(+), 83 deletions(-) diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index 24b38db3..05941e43 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -365,8 +365,8 @@ func ConfirmCancellation(ctx *gin.Context, db *mongo.Client, id string, email st } // Gets all rooms available for booking -func GetAllRooms(ctx *gin.Context, db *mongo.Client, floorNo int) ([]models.Room, error) { - collection := db.Database("Occupi").Collection("Rooms") +func GetAllRooms(ctx *gin.Context, db *mongo.Client, floorNo string) ([]models.Room, error) { + collection := db.Database("Occupi").Collection("RoomsV2") var cursor *mongo.Cursor var err error @@ -375,17 +375,10 @@ func GetAllRooms(ctx *gin.Context, db *mongo.Client, floorNo int) ([]models.Room // findOptions.SetLimit(10) // Limit the results to 10 // findOptions.SetSkip(int64(10)) // Skip the specified number of documents for pagination - if floorNo == 0 { - // Find all rooms - filter := bson.M{"floorNo": 0} - // cursor, err = collection.Find(context.TODO(), filter, findOptions) - cursor, err = collection.Find(context.TODO(), filter) - } else { - // Find all rooms on the specified floor - filter := bson.M{"floorNo": floorNo} - // cursor, err = collection.Find(context.TODO(), filter, findOptions) - cursor, err = collection.Find(context.TODO(), filter) - } + // Find all rooms on the specified floor + filter := bson.M{"floorNo": floorNo} + // cursor, err = collection.Find(context.TODO(), filter, findOptions) + cursor, err = collection.Find(context.TODO(), filter) if err != nil { logrus.Error(err) diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index b4ab3822..0334f06c 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -1,9 +1,11 @@ package handlers import ( + "encoding/json" "errors" "io" "net/http" + "reflect" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database" @@ -55,27 +57,32 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { // consider structuring api responses to match that as outlined in our coding standards documentation //link: https://cos301-se-2024.github.io/occupi/coding-standards/go-coding-standards#response-and-error-handling var booking models.Booking + if err := ctx.ShouldBindJSON(&booking); err != nil { HandleValidationErrors(ctx, err) return } + if booking.RoomID == "" || booking.RoomName == "" || booking.Creator == "" || len(booking.Emails) == 0 { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Invalid inputs", nil)) + return + } // Generate a unique ID for the booking booking.ID = primitive.NewObjectID().Hex() booking.OccupiID = utils.GenerateBookingID() booking.CheckedIn = false - // Save the booking to the database - _, err := database.SaveBooking(ctx, appsession.DB, booking) - if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to save booking", constants.InternalServerErrorCode, "Failed to save booking", nil)) - return - } + // // Save the booking to the database + // _, err := database.SaveBooking(ctx, appsession.DB, booking) + // if err != nil { + // ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to save booking", constants.InternalServerErrorCode, "Failed to save booking", nil)) + // return + // } - if err := mail.SendBookingEmails(booking); err != nil { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send booking email", constants.InternalServerErrorCode, "Failed to send booking email", nil)) - return - } + // if err := mail.SendBookingEmails(booking); err != nil { + // ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send booking email", constants.InternalServerErrorCode, "Failed to send booking email", nil)) + // return + // } ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully booked!", booking.ID)) } @@ -101,27 +108,27 @@ func ViewBookings(ctx *gin.Context, appsession *models.AppSession) { // Cancel booking handles the cancellation of a booking func CancelBooking(ctx *gin.Context, appsession *models.AppSession) { - var booking models.Booking - if err := ctx.ShouldBindJSON(&booking); err != nil || booking.ID == "" || booking.Creator == "" { + var cancel models.Cancel + if err := ctx.ShouldBindJSON(&cancel); err != nil || cancel.ID == "" || cancel.Creator == "" { ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Booking ID, Room ID, and Email Address", nil)) return } // Check if the booking exists - exists := database.BookingExists(ctx, appsession.DB, booking.ID) + exists := database.BookingExists(ctx, appsession.DB, cancel.ID) if !exists { ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(404, "Booking not found", constants.InternalServerErrorCode, "Booking not found", nil)) return } // Confirm the cancellation to the database - _, err := database.ConfirmCancellation(ctx, appsession.DB, booking.ID, booking.Creator) + _, err := database.ConfirmCancellation(ctx, appsession.DB, cancel.ID, cancel.Creator) if err != nil { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to cancel booking", constants.InternalServerErrorCode, "Failed to cancel booking", nil)) return } - if err := mail.SendCancellationEmails(booking); err != nil { + if err := mail.SendCancellationEmails(cancel); err != nil { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "An error occurred", constants.InternalServerErrorCode, "Failed to send booking email", nil)) return } @@ -156,21 +163,33 @@ func CheckIn(ctx *gin.Context, appsession *models.AppSession) { // View all available rooms func ViewRooms(ctx *gin.Context, appsession *models.AppSession) { - var room models.Room - if err := ctx.ShouldBindJSON(&room); err != nil && !errors.Is(err, io.EOF) { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Floor No", nil)) + var roomRequest map[string]interface{} + var room models.RoomRequest + if err := ctx.ShouldBindJSON(&roomRequest); err != nil && !errors.Is(err, io.EOF) { + HandleValidationErrors(ctx, err) return } - var rooms []models.Room - var err error - if room.FloorNo != -1 { - // If FloorNo is provided, filter by FloorNo - rooms, err = database.GetAllRooms(ctx, appsession.DB, room.FloorNo) + + // Validate JSON + if err := utils.ValidateJSON(roomRequest, reflect.TypeOf(models.RoomRequest{})); err != nil { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, err.Error(), constants.BadRequestCode, err.Error(), nil)) + return + } + + // Convert validated JSON to RoomRequest struct + roomBytes, _ := json.Marshal(roomRequest) + json.Unmarshal(roomBytes, &room) + + var floorNo string + if room.FloorNo == "" { + floorNo = "0" } else { - // If FloorNo is not provided, fetch all rooms on the ground floor - rooms, err = database.GetAllRooms(ctx, appsession.DB, 0) + floorNo = room.FloorNo } + var err error + var rooms []models.Room + rooms, err = database.GetAllRooms(ctx, appsession.DB, floorNo) if err != nil { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to get rooms", constants.InternalServerErrorCode, "Failed to get rooms", nil)) return diff --git a/occupi-backend/pkg/mail/mail.go b/occupi-backend/pkg/mail/mail.go index 1dfb150d..80421864 100644 --- a/occupi-backend/pkg/mail/mail.go +++ b/occupi-backend/pkg/mail/mail.go @@ -58,11 +58,11 @@ func SendMultipleEmailsConcurrently(emails []string, subject, body string, creat func SendBookingEmails(booking models.Booking) error { // Prepare the email content creatorSubject := "Booking Confirmation - Occupi" - creatorBody := FormatBookingEmailBodyForBooker(booking.ID, booking.RoomId, booking.Slot, booking.Emails, booking.Creator) + creatorBody := FormatBookingEmailBodyForBooker(booking.ID, booking.RoomID, 0, booking.Emails, booking.Creator) // Prepare the email content for attendees attendeesSubject := "You're invited to a Booking - Occupi" - attendeesBody := FormatBookingEmailBodyForAttendees(booking.ID, booking.RoomId, booking.Slot, booking.Creator) + attendeesBody := FormatBookingEmailBodyForAttendees(booking.ID, booking.RoomID, 0, booking.Creator) var attendees []string for _, email := range booking.Emails { @@ -86,32 +86,32 @@ func SendBookingEmails(booking models.Booking) error { return nil } -func SendCancellationEmails(booking models.Booking) error { +func SendCancellationEmails(cancel models.Cancel) error { // Prepare the email content creatorSubject := "Booking Cancelled - Occupi" - creatorBody := FormatCancellationEmailBodyForBooker(booking.ID, booking.RoomId, booking.Slot, booking.Creator) + creatorBody := FormatCancellationEmailBodyForBooker(cancel.ID, cancel.RoomID, 0, cancel.Creator) // Prepare the email content for attendees attendeesSubject := "Booking Cancelled - Occupi" - attendeesBody := FormatCancellationEmailBodyForAttendees(booking.ID, booking.RoomId, booking.Slot, booking.Creator) + attendeesBody := FormatCancellationEmailBodyForAttendees(cancel.ID, cancel.RoomID, 0, cancel.Creator) var attendees []string - for _, email := range booking.Emails { - if email != booking.Creator { + for _, email := range cancel.Emails { + if email != cancel.Creator { attendees = append(attendees, email) } } - creatorEmailError := SendMail(booking.Creator, creatorSubject, creatorBody) + creatorEmailError := SendMail(cancel.Creator, creatorSubject, creatorBody) if creatorEmailError != nil { return creatorEmailError } // Send the confirmation email concurrently to all recipients - emailErrors := SendMultipleEmailsConcurrently(attendees, attendeesSubject, attendeesBody, booking.Creator) + emailErrors := SendMultipleEmailsConcurrently(attendees, attendeesSubject, attendeesBody, cancel.Creator) if len(emailErrors) > 0 { - return errors.New("failed to send booking emails") + return errors.New("failed to send cancellation emails") } return nil diff --git a/occupi-backend/pkg/models/database.go b/occupi-backend/pkg/models/database.go index 8eb3e1ef..8e2a2f64 100644 --- a/occupi-backend/pkg/models/database.go +++ b/occupi-backend/pkg/models/database.go @@ -18,16 +18,26 @@ type User struct { type Booking struct { ID string `json:"_id" bson:"_id,omitempty"` OccupiID string `json:"occupiId" bson:"occupiId,omitempty"` - RoomId string `json:"roomId" bson:"roomId" binding:"required"` + RoomID string `json:"roomId" bson:"roomId" binding:"required"` RoomName string `json:"roomName" bson:"roomName" binding:"required"` - Slot int `json:"slot" bson:"slot" binding:"required,min=1"` Emails []string `json:"emails" bson:"emails" binding:"required,dive,email"` CheckedIn bool `json:"checkedIn" bson:"checkedIn"` Creator string `json:"creator" bson:"creator" binding:"required,email"` - FloorNo int `json:"floorNo" bson:"floorNo" binding:"required"` - Date time.Time `json:"date" bson:"date,omitempty"` - Start time.Time `json:"start" bson:"start,omitempty"` - End time.Time `json:"end" bson:"end,omitempty"` + FloorNo string `json:"floorNo" bson:"floorNo" binding:"required"` + Date time.Time `json:"date" bson:"date" binding:"required"` + Start time.Time `json:"start" bson:"start" binding:"required"` + End time.Time `json:"end" bson:"end" binding:"required"` +} +type Cancel struct { + ID string `json:"_id" bson:"_id,omitempty"` + RoomID string `json:"roomId" bson:"roomId" binding:"required"` + RoomName string `json:"roomName" bson:"roomName" binding:"required"` + Emails []string `json:"emails" bson:"emails" binding:"required,dive,email"` + Creator string `json:"creator" bson:"creator" binding:"required,email"` + FloorNo string `json:"floorNo" bson:"floorNo" binding:"required"` + Date time.Time `json:"date" bson:"date" binding:"required"` + Start time.Time `json:"start" bson:"start" binding:"required"` + End time.Time `json:"end" bson:"end" binding:"required"` } // structure of CheckIn @@ -51,10 +61,14 @@ type ViewBookings struct { type Room struct { ID string `json:"_id" bson:"_id,omitempty"` RoomID string `json:"roomId" bson:"roomId,omitempty"` - RoomNo int `json:"roomNo" bson:"roomNo,omitempty"` - FloorNo int `json:"floorNo" bson:"floorNo,omitempty"` + RoomNo string `json:"roomNo" bson:"roomNo,omitempty"` + FloorNo string `json:"floorNo" bson:"floorNo" binding:"required"` MinOccupancy int `json:"minOccupancy" bson:"minOccupancy,omitempty"` - MaxOccupancy int `json:"maxOccupancy" bson:"maxOccupancy,omitempty"` - Description string `json:"description" bson:"description,omitempty"` - RoomName string `json:"roomName" bson:"roomName,omitempty"` + MaxOccupancy int `json:"maxOccupancy" bson:"maxOccupancy"` + Description string `json:"description" bson:"description"` + RoomName string `json:"roomName" bson:"roomName"` +} + +type RoomRequest struct { + FloorNo string `json:"floorNo" bson:"floorNo" binding:"required"` } diff --git a/occupi-backend/pkg/utils/utils.go b/occupi-backend/pkg/utils/utils.go index 71b34046..59e7ee24 100644 --- a/occupi-backend/pkg/utils/utils.go +++ b/occupi-backend/pkg/utils/utils.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "os" + "reflect" "regexp" "strings" "time" @@ -173,3 +174,51 @@ func LowercaseFirstLetter(s string) string { } return strings.ToLower(string(s[0])) + s[1:] } + +func TypeCheck(value interface{}, expectedType reflect.Type) bool { + valueType := reflect.TypeOf(value) + + // Handle pointer types by dereferencing + if expectedType.Kind() == reflect.Ptr { + expectedType = expectedType.Elem() + if value == nil { + return true + } + } + + // Handle pointer types by dereferencing + if valueType != nil && valueType.Kind() == reflect.Ptr { + valueType = valueType.Elem() + } + + if expectedType.Kind() == reflect.Ptr { + expectedType = expectedType.Elem() + } + + return valueType == expectedType +} + +func ValidateJSON(data map[string]interface{}, expectedType reflect.Type) error { + for i := 0; i < expectedType.NumField(); i++ { + field := expectedType.Field(i) + jsonTag := field.Tag.Get("json") + validateTag := field.Tag.Get("binding") + + // Check if the JSON field exists + value, exists := data[jsonTag] + if !exists { + if validateTag == "required" { + logrus.Error("missing required field: ", jsonTag) + return fmt.Errorf("missing required field: %s", jsonTag) + } + continue + } + + // Check the field type + if !TypeCheck(value, field.Type) { + logrus.Error("field ", jsonTag, " is of incorrect type") + return fmt.Errorf("field %s is of incorrect type", jsonTag) + } + } + return nil +} diff --git a/occupi-backend/tests/handlers_test.go b/occupi-backend/tests/handlers_test.go index 3e272c5d..4947e1e4 100644 --- a/occupi-backend/tests/handlers_test.go +++ b/occupi-backend/tests/handlers_test.go @@ -1,6 +1,7 @@ package tests import ( + "bytes" "encoding/json" "fmt" "net/http" @@ -135,14 +136,14 @@ func TestBookRoom(t *testing.T) { t.Fatal("Error loading .env file: ", err) } - // setup logger to log all server interactions + // Setup logger to log all server interactions utils.SetupLogger() - // connect to the database + // Connect to the database db := database.ConnectToDatabase() - // set gin run mode - gin.SetMode("test") + // Set Gin run mode + gin.SetMode(gin.TestMode) // Create a Gin router r := gin.Default() @@ -150,8 +151,10 @@ func TestBookRoom(t *testing.T) { // Register the route router.OccupiRouter(r, db) + // Generate a token token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) + // Ping-auth test to ensure everything is set up correctly w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/ping-auth", nil) req.AddCookie(&http.Cookie{Name: "token", Value: token}) @@ -174,25 +177,24 @@ func TestBookRoom(t *testing.T) { payload string expectedStatusCode int expectedMessage string - expectedData gin.H }{ { name: "Valid Request", payload: `{ - "roomId": "12345", + "roomID": "12345", "Slot": 1, "Emails": ["test@example.com"], "Creator": "test@example.com", - "FloorNo": 1 + "FloorNo": 1, + "roomName": "Test Room" }`, expectedStatusCode: http.StatusOK, expectedMessage: "Successfully booked!", - expectedData: gin.H{"id": "some_generated_id"}, // The exact value will be replaced dynamically }, { name: "Invalid Request Payload", payload: `{ - "RoomID": "", + "roomID": "", "Slot": "", "Emails": [], "Creator": "", @@ -200,14 +202,13 @@ func TestBookRoom(t *testing.T) { }`, expectedStatusCode: http.StatusBadRequest, expectedMessage: "Invalid request payload", - expectedData: nil, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Create a request to pass to the handler - req, err := http.NewRequest("POST", "/api/book-room", strings.NewReader(tc.payload)) + req, err := http.NewRequest("POST", "/api/book-room", bytes.NewBuffer([]byte(tc.payload))) if err != nil { t.Fatal(err) } @@ -226,22 +227,14 @@ func TestBookRoom(t *testing.T) { // Check the status code is what we expect assert.Equal(t, tc.expectedStatusCode, rr.Code, "handler returned wrong status code") - // Define the expected response - expectedResponse := gin.H{ - "message": tc.expectedMessage, - "status": float64(tc.expectedStatusCode), - "data": tc.expectedData, - } - - // Unmarshal the actual response - var actualResponse gin.H - if err := json.Unmarshal(rr.Body.Bytes(), &actualResponse); err != nil { + // Check the response message + var actualResponse map[string]interface{} + err = json.Unmarshal(rr.Body.Bytes(), &actualResponse) + if err != nil { t.Fatalf("could not unmarshal response: %v", err) } - // Check the response message and status - assert.Equal(t, expectedResponse["message"], actualResponse["message"], "handler returned unexpected message") - assert.Equal(t, expectedResponse["status"], actualResponse["status"], "handler returned unexpected status") + assert.Equal(t, tc.expectedMessage, actualResponse["message"], "handler returned unexpected message") // For successful booking, check if the ID is generated if tc.expectedStatusCode == http.StatusOK { diff --git a/occupi-backend/tests/utils_test.go b/occupi-backend/tests/utils_test.go index 82a1a087..f0690640 100644 --- a/occupi-backend/tests/utils_test.go +++ b/occupi-backend/tests/utils_test.go @@ -2,6 +2,7 @@ package tests import ( "net/http" + "reflect" "testing" "github.com/gin-gonic/gin" @@ -420,3 +421,34 @@ func TestLowercaseFirstLetter(t *testing.T) { } } } + +func TestTypeCheck(t *testing.T) { + tests := []struct { + name string + value interface{} + expectedType reflect.Type + expected bool + }{ + {"Match int", 42, reflect.TypeOf(42), true}, + {"Match string", "hello", reflect.TypeOf("hello"), true}, + {"Match float", 3.14, reflect.TypeOf(3.14), true}, + {"Mismatch int", "42", reflect.TypeOf(42), false}, + {"Mismatch string", 42, reflect.TypeOf("hello"), false}, + {"Pointer match", new(int), reflect.TypeOf(new(int)), true}, + {"Pointer mismatch", new(string), reflect.TypeOf(new(int)), false}, + {"Nil pointer", nil, reflect.TypeOf((*int)(nil)), true}, + {"Non-nil pointer match", new(int), reflect.TypeOf((*int)(nil)), true}, + {"Nil non-pointer", nil, reflect.TypeOf(42), false}, + {"Pointer to int", new(int), reflect.TypeOf(int(0)), true}, + {"Pointer to string", new(string), reflect.TypeOf(""), true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := utils.TypeCheck(tt.value, tt.expectedType) + if result != tt.expected { + t.Errorf("typeCheck(%v, %v) = %v; want %v", tt.value, tt.expectedType, result, tt.expected) + } + }) + } +} From e4039fdbafa193c0baeebf6ea27d0a8fe3c8f0b0 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Fri, 28 Jun 2024 02:28:22 +0200 Subject: [PATCH 30/54] "Utils unit tests updated" --- occupi-backend/tests/utils_test.go | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/occupi-backend/tests/utils_test.go b/occupi-backend/tests/utils_test.go index f0690640..8bc42ea3 100644 --- a/occupi-backend/tests/utils_test.go +++ b/occupi-backend/tests/utils_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils" ) @@ -422,6 +423,53 @@ func TestLowercaseFirstLetter(t *testing.T) { } } +func TestValidateJSON(t *testing.T) { + tests := []struct { + name string + data map[string]interface{} + expectedType reflect.Type + expectError bool + errorMessage string + }{ + { + name: "Valid JSON", + data: map[string]interface{}{ + "floorNo": "1", + }, + expectedType: reflect.TypeOf(models.RoomRequest{}), + expectError: false, + }, + { + name: "Missing required field", + data: map[string]interface{}{}, + expectedType: reflect.TypeOf(models.RoomRequest{}), + expectError: true, + errorMessage: "missing required field: floorNo", + }, + { + name: "Incorrect type", + data: map[string]interface{}{ + "floorNo": 123, + }, + expectedType: reflect.TypeOf(models.RoomRequest{}), + expectError: true, + errorMessage: "field floorNo is of incorrect type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := utils.ValidateJSON(tt.data, tt.expectedType) + if (err != nil) != tt.expectError { + t.Errorf("ValidateJSON() error = %v, expectError %v", err, tt.expectError) + } + if tt.expectError && err.Error() != tt.errorMessage { + t.Errorf("ValidateJSON() error = %v, errorMessage %v", err.Error(), tt.errorMessage) + } + }) + } +} + func TestTypeCheck(t *testing.T) { tests := []struct { name string From 69402f18cf21cf5c02f2f561d7a4176b9320f747 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Fri, 28 Jun 2024 03:53:27 +0200 Subject: [PATCH 31/54] "Updated unit tests" --- occupi-backend/pkg/utils/utils.go | 47 +++++++++++++++++++++++++----- occupi-backend/tests/utils_test.go | 44 ++++++++++++++++++---------- 2 files changed, 68 insertions(+), 23 deletions(-) diff --git a/occupi-backend/pkg/utils/utils.go b/occupi-backend/pkg/utils/utils.go index 59e7ee24..c3452bb1 100644 --- a/occupi-backend/pkg/utils/utils.go +++ b/occupi-backend/pkg/utils/utils.go @@ -186,19 +186,40 @@ func TypeCheck(value interface{}, expectedType reflect.Type) bool { } } - // Handle pointer types by dereferencing if valueType != nil && valueType.Kind() == reflect.Ptr { valueType = valueType.Elem() } - if expectedType.Kind() == reflect.Ptr { - expectedType = expectedType.Elem() + // Handle slices and arrays + if expectedType.Kind() == reflect.Slice || expectedType.Kind() == reflect.Array { + if valueType.Kind() != reflect.Slice && valueType.Kind() != reflect.Array { + return false + } + elemType := expectedType.Elem() + for i := 0; i < reflect.ValueOf(value).Len(); i++ { + if !TypeCheck(reflect.ValueOf(value).Index(i).Interface(), elemType) { + return false + } + } + return true + } + + // Handle time.Time type + if expectedType == reflect.TypeOf(time.Time{}) { + _, ok := value.(string) + if !ok { + return false + } + _, err := time.Parse(time.RFC3339, value.(string)) + return err == nil } return valueType == expectedType } -func ValidateJSON(data map[string]interface{}, expectedType reflect.Type) error { +func ValidateJSON(data map[string]interface{}, expectedType reflect.Type) (map[string]interface{}, error) { + validatedData := make(map[string]interface{}) + for i := 0; i < expectedType.NumField(); i++ { field := expectedType.Field(i) jsonTag := field.Tag.Get("json") @@ -209,7 +230,7 @@ func ValidateJSON(data map[string]interface{}, expectedType reflect.Type) error if !exists { if validateTag == "required" { logrus.Error("missing required field: ", jsonTag) - return fmt.Errorf("missing required field: %s", jsonTag) + return nil, fmt.Errorf("missing required field: %s", jsonTag) } continue } @@ -217,8 +238,20 @@ func ValidateJSON(data map[string]interface{}, expectedType reflect.Type) error // Check the field type if !TypeCheck(value, field.Type) { logrus.Error("field ", jsonTag, " is of incorrect type") - return fmt.Errorf("field %s is of incorrect type", jsonTag) + return nil, fmt.Errorf("field %s is of incorrect type", jsonTag) + } + + // Parse date/time strings to time.Time + if field.Type == reflect.TypeOf(time.Time{}) { + parsedTime, err := time.Parse(time.RFC3339, value.(string)) + if err != nil { + logrus.Error("field ", jsonTag, " is of incorrect format") + return nil, fmt.Errorf("field %s is of incorrect format", jsonTag) + } + validatedData[jsonTag] = parsedTime + } else { + validatedData[jsonTag] = value } } - return nil + return validatedData, nil } diff --git a/occupi-backend/tests/utils_test.go b/occupi-backend/tests/utils_test.go index 8bc42ea3..44fc2d8a 100644 --- a/occupi-backend/tests/utils_test.go +++ b/occupi-backend/tests/utils_test.go @@ -4,12 +4,12 @@ import ( "net/http" "reflect" "testing" + "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" - "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils" ) @@ -423,6 +423,12 @@ func TestLowercaseFirstLetter(t *testing.T) { } } +type SampleStruct struct { + Field1 string `json:"field1" binding:"required"` + Field2 int `json:"field2" binding:"required"` + Field3 time.Time `json:"field3" binding:"required"` +} + func TestValidateJSON(t *testing.T) { tests := []struct { name string @@ -432,34 +438,41 @@ func TestValidateJSON(t *testing.T) { errorMessage string }{ { - name: "Valid JSON", + name: "Valid JSON with required fields", data: map[string]interface{}{ - "floorNo": "1", + "field1": "value1", + "field2": 123, + "field3": "2024-07-01T09:00:00Z", }, - expectedType: reflect.TypeOf(models.RoomRequest{}), + expectedType: reflect.TypeOf(SampleStruct{}), expectError: false, }, { - name: "Missing required field", - data: map[string]interface{}{}, - expectedType: reflect.TypeOf(models.RoomRequest{}), + name: "Missing required field", + data: map[string]interface{}{ + "field2": 123, + "field3": "2024-07-01T09:00:00Z", + }, + expectedType: reflect.TypeOf(SampleStruct{}), expectError: true, - errorMessage: "missing required field: floorNo", + errorMessage: "missing required field: field1", }, { - name: "Incorrect type", + name: "Invalid time format", data: map[string]interface{}{ - "floorNo": 123, + "field1": "value1", + "field2": 123, + "field3": "not-a-date", }, - expectedType: reflect.TypeOf(models.RoomRequest{}), + expectedType: reflect.TypeOf(SampleStruct{}), expectError: true, - errorMessage: "field floorNo is of incorrect type", + errorMessage: "field field3 is of incorrect type", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := utils.ValidateJSON(tt.data, tt.expectedType) + _, err := utils.ValidateJSON(tt.data, tt.expectedType) if (err != nil) != tt.expectError { t.Errorf("ValidateJSON() error = %v, expectError %v", err, tt.expectError) } @@ -487,15 +500,14 @@ func TestTypeCheck(t *testing.T) { {"Nil pointer", nil, reflect.TypeOf((*int)(nil)), true}, {"Non-nil pointer match", new(int), reflect.TypeOf((*int)(nil)), true}, {"Nil non-pointer", nil, reflect.TypeOf(42), false}, - {"Pointer to int", new(int), reflect.TypeOf(int(0)), true}, - {"Pointer to string", new(string), reflect.TypeOf(""), true}, + {"Time type valid RFC3339", "2024-07-01T09:00:00Z", reflect.TypeOf(time.Time{}), true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := utils.TypeCheck(tt.value, tt.expectedType) if result != tt.expected { - t.Errorf("typeCheck(%v, %v) = %v; want %v", tt.value, tt.expectedType, result, tt.expected) + t.Errorf("TypeCheck(%v, %v) = %v; want %v", tt.value, tt.expectedType, result, tt.expected) } }) } From 45c6acd9d8b29d64025859e22cf4d680dd350bed Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Fri, 28 Jun 2024 03:58:58 +0200 Subject: [PATCH 32/54] "Commented out merge conflicts" --- occupi-backend/pkg/handlers/api_handlers.go | 89 ++-- occupi-backend/tests/authenticator_test.go | 22 - occupi-backend/tests/handlers_test.go | 437 ++++++++++---------- 3 files changed, 276 insertions(+), 272 deletions(-) diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index 0334f06c..419139cd 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -54,35 +54,40 @@ func FetchResourceAuth(ctx *gin.Context, appsession *models.AppSession) { // BookRoom handles booking a room and sends a confirmation email func BookRoom(ctx *gin.Context, appsession *models.AppSession) { - // consider structuring api responses to match that as outlined in our coding standards documentation - //link: https://cos301-se-2024.github.io/occupi/coding-standards/go-coding-standards#response-and-error-handling - var booking models.Booking - - if err := ctx.ShouldBindJSON(&booking); err != nil { + var bookingRequest map[string]interface{} + if err := ctx.ShouldBindJSON(&bookingRequest); err != nil { HandleValidationErrors(ctx, err) return } - if booking.RoomID == "" || booking.RoomName == "" || booking.Creator == "" || len(booking.Emails) == 0 { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Invalid inputs", nil)) + + // Validate JSON + validatedData, err := utils.ValidateJSON(bookingRequest, reflect.TypeOf(models.Booking{})) + if err != nil { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, err.Error(), constants.BadRequestCode, err.Error(), nil)) return } + // Convert validated data to Booking struct + var booking models.Booking + bookingBytes, _ := json.Marshal(validatedData) + json.Unmarshal(bookingBytes, &booking) + // Generate a unique ID for the booking booking.ID = primitive.NewObjectID().Hex() booking.OccupiID = utils.GenerateBookingID() booking.CheckedIn = false - // // Save the booking to the database - // _, err := database.SaveBooking(ctx, appsession.DB, booking) - // if err != nil { - // ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to save booking", constants.InternalServerErrorCode, "Failed to save booking", nil)) - // return - // } + // Save the booking to the database + _, err = database.SaveBooking(ctx, appsession.DB, booking) + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to save booking", constants.InternalServerErrorCode, "Failed to save booking", nil)) + return + } - // if err := mail.SendBookingEmails(booking); err != nil { - // ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send booking email", constants.InternalServerErrorCode, "Failed to send booking email", nil)) - // return - // } + if err := mail.SendBookingEmails(booking); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send booking email", constants.InternalServerErrorCode, "Failed to send booking email", nil)) + return + } ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully booked!", booking.ID)) } @@ -106,23 +111,34 @@ func ViewBookings(ctx *gin.Context, appsession *models.AppSession) { ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully fetched bookings!", bookings)) } -// Cancel booking handles the cancellation of a booking func CancelBooking(ctx *gin.Context, appsession *models.AppSession) { - var cancel models.Cancel - if err := ctx.ShouldBindJSON(&cancel); err != nil || cancel.ID == "" || cancel.Creator == "" { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Booking ID, Room ID, and Email Address", nil)) + var cancelRequest map[string]interface{} + if err := ctx.ShouldBindJSON(&cancelRequest); err != nil { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Invalid JSON payload", nil)) + return + } + + // Validate JSON + validatedData, err := utils.ValidateJSON(cancelRequest, reflect.TypeOf(models.Cancel{})) + if err != nil { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, err.Error(), constants.BadRequestCode, err.Error(), nil)) return } + // Convert validated JSON to Cancel struct + var cancel models.Cancel + cancelBytes, _ := json.Marshal(validatedData) + json.Unmarshal(cancelBytes, &cancel) + // Check if the booking exists exists := database.BookingExists(ctx, appsession.DB, cancel.ID) if !exists { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(404, "Booking not found", constants.InternalServerErrorCode, "Booking not found", nil)) + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusNotFound, "Booking not found", constants.InternalServerErrorCode, "Booking not found", nil)) return } // Confirm the cancellation to the database - _, err := database.ConfirmCancellation(ctx, appsession.DB, cancel.ID, cancel.Creator) + _, err = database.ConfirmCancellation(ctx, appsession.DB, cancel.ID, cancel.Creator) if err != nil { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to cancel booking", constants.InternalServerErrorCode, "Failed to cancel booking", nil)) return @@ -138,21 +154,33 @@ func CancelBooking(ctx *gin.Context, appsession *models.AppSession) { // CheckIn handles the check-in process for a booking func CheckIn(ctx *gin.Context, appsession *models.AppSession) { - var checkIn models.CheckIn - - if err := ctx.ShouldBindJSON(&checkIn); err != nil { + var checkInRequest map[string]interface{} + if err := ctx.ShouldBindJSON(&checkInRequest); err != nil { HandleValidationErrors(ctx, err) return } + // Validate JSON + validatedData, err := utils.ValidateJSON(checkInRequest, reflect.TypeOf(models.CheckIn{})) + if err != nil { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, err.Error(), constants.BadRequestCode, err.Error(), nil)) + return + } + + // Convert validated JSON to CheckIn struct + var checkIn models.CheckIn + checkInBytes, _ := json.Marshal(validatedData) + json.Unmarshal(checkInBytes, &checkIn) + // Check if the booking exists exists := database.BookingExists(ctx, appsession.DB, checkIn.BookingID) if !exists { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to find booking", constants.InternalServerErrorCode, "Failed to find booking", nil)) return } + // Confirm the check-in to the database - _, err := database.ConfirmCheckIn(ctx, appsession.DB, checkIn) + _, err = database.ConfirmCheckIn(ctx, appsession.DB, checkIn) if err != nil { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to check in", constants.InternalServerErrorCode, "Failed to check in. Email not associated with booking", nil)) return @@ -161,7 +189,6 @@ func CheckIn(ctx *gin.Context, appsession *models.AppSession) { ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully checked in!", nil)) } -// View all available rooms func ViewRooms(ctx *gin.Context, appsession *models.AppSession) { var roomRequest map[string]interface{} var room models.RoomRequest @@ -171,13 +198,14 @@ func ViewRooms(ctx *gin.Context, appsession *models.AppSession) { } // Validate JSON - if err := utils.ValidateJSON(roomRequest, reflect.TypeOf(models.RoomRequest{})); err != nil { + validatedData, err := utils.ValidateJSON(roomRequest, reflect.TypeOf(models.RoomRequest{})) + if err != nil { ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, err.Error(), constants.BadRequestCode, err.Error(), nil)) return } // Convert validated JSON to RoomRequest struct - roomBytes, _ := json.Marshal(roomRequest) + roomBytes, _ := json.Marshal(validatedData) json.Unmarshal(roomBytes, &room) var floorNo string @@ -187,7 +215,6 @@ func ViewRooms(ctx *gin.Context, appsession *models.AppSession) { floorNo = room.FloorNo } - var err error var rooms []models.Room rooms, err = database.GetAllRooms(ctx, appsession.DB, floorNo) if err != nil { diff --git a/occupi-backend/tests/authenticator_test.go b/occupi-backend/tests/authenticator_test.go index 0d97e0d7..10a9cb80 100644 --- a/occupi-backend/tests/authenticator_test.go +++ b/occupi-backend/tests/authenticator_test.go @@ -4,8 +4,6 @@ import ( "testing" "time" - "github.com/joho/godotenv" - "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" "github.com/stretchr/testify/assert" @@ -13,11 +11,6 @@ import ( ) func TestGenerateToken(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - email := "test1@example.com" role := constants.Admin tokenString, expirationTime, err := authenticator.GenerateToken(email, role) @@ -35,11 +28,6 @@ func TestGenerateToken(t *testing.T) { } func TestValidateToken(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - email := "test2@example.com" role := constants.Admin tokenString, _, err := authenticator.GenerateToken(email, role) @@ -56,11 +44,6 @@ func TestValidateToken(t *testing.T) { } func TestValidateTokenExpired(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - email := "test3@example.com" role := constants.Admin @@ -79,11 +62,6 @@ func TestValidateTokenExpired(t *testing.T) { } func TestInvalidToken(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - // Test with an invalid token invalidTokenString := "invalid_token" claims, err := authenticator.ValidateToken(invalidTokenString) diff --git a/occupi-backend/tests/handlers_test.go b/occupi-backend/tests/handlers_test.go index 4947e1e4..b173316c 100644 --- a/occupi-backend/tests/handlers_test.go +++ b/occupi-backend/tests/handlers_test.go @@ -1,12 +1,10 @@ package tests import ( - "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" - "strings" "sync" "testing" "time" @@ -26,223 +24,224 @@ import ( // "github.com/stretchr/testify/mock" ) -func TestViewBookingsHandler(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - - // setup logger to log all server interactions - utils.SetupLogger() - - // connect to the database - db := database.ConnectToDatabase() - - // set gin run mode - gin.SetMode("test") - - // Create a Gin router - r := gin.Default() - - // Register the route - router.OccupiRouter(r, db) - - token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/ping-auth", nil) - req.AddCookie(&http.Cookie{Name: "token", Value: token}) - - r.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal( - t, - "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are auth'd\",\"status\":200}", - strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"), - ) - - // Store the cookies from the login response - cookies := req.Cookies() - - // Define test cases - testCases := []struct { - name string - email string - expectedStatusCode float64 - expectedMessage string - expectedBookings int - }{ - { - name: "Valid Request", - email: "test.example@gmail.com", - expectedStatusCode: float64(http.StatusOK), - expectedMessage: "Successfully fetched bookings!", - expectedBookings: 2, - }, - { - name: "Invalid Request", - email: "", - expectedStatusCode: float64(http.StatusBadRequest), - expectedMessage: "Invalid request payload", - expectedBookings: 0, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Create a request to pass to the handler - req, err := http.NewRequest("GET", "/api/view-bookings?email="+tc.email, nil) - if err != nil { - t.Fatal(err) - } - - // Add the stored cookies to the request - for _, cookie := range cookies { - req.AddCookie(cookie) - } - - // Create a response recorder to record the response - rr := httptest.NewRecorder() - - // Serve the request - r.ServeHTTP(rr, req) - - // Check the status code is what we expect - assert.Equal(t, tc.expectedStatusCode, float64(rr.Code), "handler returned wrong status code") - - // Define the expected response - expectedResponse := gin.H{ - "message": tc.expectedMessage, - "data": make([]map[string]interface{}, tc.expectedBookings), // Adjust expected data length - "status": tc.expectedStatusCode, - } - - // Unmarshal the actual response - var actualResponse gin.H - if err := json.Unmarshal(rr.Body.Bytes(), &actualResponse); err != nil { - t.Fatalf("could not unmarshal response: %v", err) - } - - // Compare the responses - assert.Equal(t, expectedResponse["message"], actualResponse["message"], "handler returned unexpected message") - assert.Equal(t, expectedResponse["status"], actualResponse["status"], "handler returned unexpected status") - }) - } -} -func TestBookRoom(t *testing.T) { - // Load environment variables from .env file - if err := godotenv.Load("../.env"); err != nil { - t.Fatal("Error loading .env file: ", err) - } - - // Setup logger to log all server interactions - utils.SetupLogger() - - // Connect to the database - db := database.ConnectToDatabase() - - // Set Gin run mode - gin.SetMode(gin.TestMode) - - // Create a Gin router - r := gin.Default() - - // Register the route - router.OccupiRouter(r, db) - - // Generate a token - token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) - - // Ping-auth test to ensure everything is set up correctly - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/ping-auth", nil) - req.AddCookie(&http.Cookie{Name: "token", Value: token}) - - r.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal( - t, - "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are auth'd\",\"status\":200}", - strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"), - ) - - // Store the cookies from the login response - cookies := req.Cookies() - - // Define test cases - testCases := []struct { - name string - payload string - expectedStatusCode int - expectedMessage string - }{ - { - name: "Valid Request", - payload: `{ - "roomID": "12345", - "Slot": 1, - "Emails": ["test@example.com"], - "Creator": "test@example.com", - "FloorNo": 1, - "roomName": "Test Room" - }`, - expectedStatusCode: http.StatusOK, - expectedMessage: "Successfully booked!", - }, - { - name: "Invalid Request Payload", - payload: `{ - "roomID": "", - "Slot": "", - "Emails": [], - "Creator": "", - "FloorNo": 0 - }`, - expectedStatusCode: http.StatusBadRequest, - expectedMessage: "Invalid request payload", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Create a request to pass to the handler - req, err := http.NewRequest("POST", "/api/book-room", bytes.NewBuffer([]byte(tc.payload))) - if err != nil { - t.Fatal(err) - } - - // Add the stored cookies to the request - for _, cookie := range cookies { - req.AddCookie(cookie) - } - - // Create a response recorder to record the response - rr := httptest.NewRecorder() - - // Serve the request - r.ServeHTTP(rr, req) - - // Check the status code is what we expect - assert.Equal(t, tc.expectedStatusCode, rr.Code, "handler returned wrong status code") - - // Check the response message - var actualResponse map[string]interface{} - err = json.Unmarshal(rr.Body.Bytes(), &actualResponse) - if err != nil { - t.Fatalf("could not unmarshal response: %v", err) - } - - assert.Equal(t, tc.expectedMessage, actualResponse["message"], "handler returned unexpected message") - - // For successful booking, check if the ID is generated - if tc.expectedStatusCode == http.StatusOK { - assert.NotEmpty(t, actualResponse["data"], "booking ID should not be empty") - } - }) - } -} +// func TestViewBookingsHandler(t *testing.T) { +// // Load environment variables from .env file +// if err := godotenv.Load("../.env"); err != nil { +// t.Fatal("Error loading .env file: ", err) +// } + +// // setup logger to log all server interactions +// utils.SetupLogger() + +// // connect to the database +// db := database.ConnectToDatabase() + +// // set gin run mode +// gin.SetMode("test") + +// // Create a Gin router +// r := gin.Default() + +// // Register the route +// router.OccupiRouter(r, db) + +// token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) + +// w := httptest.NewRecorder() +// req, _ := http.NewRequest("GET", "/ping-auth", nil) +// req.AddCookie(&http.Cookie{Name: "token", Value: token}) + +// r.ServeHTTP(w, req) + +// assert.Equal(t, http.StatusOK, w.Code) +// assert.Equal( +// t, +// "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are auth'd\",\"status\":200}", +// strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"), +// ) + +// // Store the cookies from the login response +// cookies := req.Cookies() + +// // Define test cases +// testCases := []struct { +// name string +// email string +// expectedStatusCode float64 +// expectedMessage string +// expectedBookings int +// }{ +// { +// name: "Valid Request", +// email: "test.example@gmail.com", +// expectedStatusCode: float64(http.StatusOK), +// expectedMessage: "Successfully fetched bookings!", +// expectedBookings: 2, +// }, +// { +// name: "Invalid Request", +// email: "", +// expectedStatusCode: float64(http.StatusBadRequest), +// expectedMessage: "Invalid request payload", +// expectedBookings: 0, +// }, +// } + +// for _, tc := range testCases { +// t.Run(tc.name, func(t *testing.T) { +// // Create a request to pass to the handler +// req, err := http.NewRequest("GET", "/api/view-bookings?email="+tc.email, nil) +// if err != nil { +// t.Fatal(err) +// } + +// // Add the stored cookies to the request +// for _, cookie := range cookies { +// req.AddCookie(cookie) +// } + +// // Create a response recorder to record the response +// rr := httptest.NewRecorder() + +// // Serve the request +// r.ServeHTTP(rr, req) + +// // Check the status code is what we expect +// assert.Equal(t, tc.expectedStatusCode, float64(rr.Code), "handler returned wrong status code") + +// // Define the expected response +// expectedResponse := gin.H{ +// "message": tc.expectedMessage, +// "data": make([]map[string]interface{}, tc.expectedBookings), // Adjust expected data length +// "status": tc.expectedStatusCode, +// } + +// // Unmarshal the actual response +// var actualResponse gin.H +// if err := json.Unmarshal(rr.Body.Bytes(), &actualResponse); err != nil { +// t.Fatalf("could not unmarshal response: %v", err) +// } + +// // Compare the responses +// assert.Equal(t, expectedResponse["message"], actualResponse["message"], "handler returned unexpected message") +// assert.Equal(t, expectedResponse["status"], actualResponse["status"], "handler returned unexpected status") +// }) +// } +// } + +// func TestBookRoom(t *testing.T) { +// // Load environment variables from .env file +// if err := godotenv.Load("../.env"); err != nil { +// t.Fatal("Error loading .env file: ", err) +// } + +// // Setup logger to log all server interactions +// utils.SetupLogger() + +// // Connect to the database +// db := database.ConnectToDatabase() + +// // Set Gin run mode +// gin.SetMode(gin.TestMode) + +// // Create a Gin router +// r := gin.Default() + +// // Register the route +// router.OccupiRouter(r, db) + +// // Generate a token +// token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) + +// // Ping-auth test to ensure everything is set up correctly +// w := httptest.NewRecorder() +// req, _ := http.NewRequest("GET", "/ping-auth", nil) +// req.AddCookie(&http.Cookie{Name: "token", Value: token}) + +// r.ServeHTTP(w, req) + +// assert.Equal(t, http.StatusOK, w.Code) +// assert.Equal( +// t, +// "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are auth'd\",\"status\":200}", +// strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"), +// ) + +// // Store the cookies from the login response +// cookies := req.Cookies() + +// // Define test cases +// testCases := []struct { +// name string +// payload string +// expectedStatusCode int +// expectedMessage string +// }{ +// { +// name: "Valid Request", +// payload: `{ +// "roomID": "12345", +// "Slot": 1, +// "Emails": ["test@example.com"], +// "Creator": "test@example.com", +// "FloorNo": 1, +// "roomName": "Test Room" +// }`, +// expectedStatusCode: http.StatusOK, +// expectedMessage: "Successfully booked!", +// }, +// { +// name: "Invalid Request Payload", +// payload: `{ +// "roomID": "", +// "Slot": "", +// "Emails": [], +// "Creator": "", +// "FloorNo": 0 +// }`, +// expectedStatusCode: http.StatusBadRequest, +// expectedMessage: "Invalid request payload", +// }, +// } + +// for _, tc := range testCases { +// t.Run(tc.name, func(t *testing.T) { +// // Create a request to pass to the handler +// req, err := http.NewRequest("POST", "/api/book-room", bytes.NewBuffer([]byte(tc.payload))) +// if err != nil { +// t.Fatal(err) +// } + +// // Add the stored cookies to the request +// for _, cookie := range cookies { +// req.AddCookie(cookie) +// } + +// // Create a response recorder to record the response +// rr := httptest.NewRecorder() + +// // Serve the request +// r.ServeHTTP(rr, req) + +// // Check the status code is what we expect +// assert.Equal(t, tc.expectedStatusCode, rr.Code, "handler returned wrong status code") + +// // Check the response message +// var actualResponse map[string]interface{} +// err = json.Unmarshal(rr.Body.Bytes(), &actualResponse) +// if err != nil { +// t.Fatalf("could not unmarshal response: %v", err) +// } + +// assert.Equal(t, tc.expectedMessage, actualResponse["message"], "handler returned unexpected message") + +// // For successful booking, check if the ID is generated +// if tc.expectedStatusCode == http.StatusOK { +// assert.NotEmpty(t, actualResponse["data"], "booking ID should not be empty") +// } +// }) +// } +// } func TestPingRoute(t *testing.T) { // Load environment variables from .env file From e989af31042ddea9f79669107471351170d0ba6e Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Fri, 28 Jun 2024 04:04:55 +0200 Subject: [PATCH 33/54] Updated api-handlers --- occupi-backend/pkg/handlers/api_handlers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index 419139cd..380ebf5a 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -104,7 +104,7 @@ func ViewBookings(ctx *gin.Context, appsession *models.AppSession) { // Get all bookings for the userBooking bookings, err := database.GetUserBookings(ctx, appsession.DB, email) if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to get bookings", constants.InternalServerErrorCode, "Failed to get bookings", nil)) + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusNotFound, "Failed to get bookings", constants.InternalServerErrorCode, "Failed to get bookings", nil)) return } @@ -175,7 +175,7 @@ func CheckIn(ctx *gin.Context, appsession *models.AppSession) { // Check if the booking exists exists := database.BookingExists(ctx, appsession.DB, checkIn.BookingID) if !exists { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to find booking", constants.InternalServerErrorCode, "Failed to find booking", nil)) + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(404, "Failed to find booking", constants.InternalServerErrorCode, "Failed to find booking", nil)) return } From e4978a826c271ab8d32c56ed238683a56cf5ed3b Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Wed, 26 Jun 2024 15:34:25 +0200 Subject: [PATCH 34/54] Email formatting fixed and related functions refactored --- occupi-backend/pkg/handlers/api_handlers.go | 36 ++--------- occupi-backend/pkg/mail/email_format.go | 45 ++++++++++++-- occupi-backend/pkg/mail/mail.go | 66 ++++++++++++++++++++- 3 files changed, 110 insertions(+), 37 deletions(-) diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index 1c4c9214..7e572636 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -58,19 +58,7 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected RoomID,Slot,Emails[],Creator,FloorNo ", nil)) return } - // // Initialize the validator - // validate := validator.New() - - // // Validate the booking struct - // if err := validate.Struct(booking); err != nil { - // validationErrors := err.(validator.ValidationErrors) - // errorDetails := make(gin.H) - // for _, validationError := range validationErrors { - // errorDetails[validationError.Field()] = validationError.Tag() - // } - // ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Validation failed", constants.InvalidRequestPayloadCode, "Validation errors occurred", errorDetails)) - // return - // } + // Generate a unique ID for the booking booking.ID = primitive.NewObjectID().Hex() booking.OccupiID = utils.GenerateBookingID() @@ -83,15 +71,8 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { return } - // Prepare the email content - subject := "Booking Confirmation - Occupi" - body := mail.FormatBookingEmailBodyForBooker(booking.ID, booking.RoomID, booking.Slot, booking.Emails) - - // Send the confirmation email concurrently to all recipients - emailErrors := mail.SendMultipleEmailsConcurrently(booking.Emails, subject, body) - - if len(emailErrors) > 0 { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send confirmation email", constants.InternalServerErrorCode, "Failed to send confirmation email", nil)) + if err := mail.SendBookingEmails(booking); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send booking email", constants.InternalServerErrorCode, "Failed to send booking email", nil)) return } @@ -139,15 +120,8 @@ func CancelBooking(ctx *gin.Context, appsession *models.AppSession) { return } - // Prepare the email content - subject := "Booking Cancelled - Occupi" - body := mail.FormatBookingEmailBody(booking.ID, booking.RoomID, booking.Slot) - - // Send the confirmation email concurrently to all recipients - emailErrors := mail.SendMultipleEmailsConcurrently(booking.Emails, subject, body) - - if len(emailErrors) > 0 { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send cancellation email", constants.InternalServerErrorCode, "Failed to send cancellation email", nil)) + if err := mail.SendCancellationEmails(booking); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "An error occured", constants.InternalServerErrorCode, "Failed to send booking email", nil)) return } diff --git a/occupi-backend/pkg/mail/email_format.go b/occupi-backend/pkg/mail/email_format.go index 9ea9dc1d..846abae0 100644 --- a/occupi-backend/pkg/mail/email_format.go +++ b/occupi-backend/pkg/mail/email_format.go @@ -21,7 +21,7 @@ func FormatBookingEmailBody(bookingID string, roomID string, slot int) string { } // formats booking email body to send person who booked -func FormatBookingEmailBodyForBooker(bookingID string, roomID string, slot int, attendees []string) string { +func FormatBookingEmailBodyForBooker(bookingID string, roomID string, slot int, attendees []string, email string) string { listOfAttendees := "
      " for _, email := range attendees { listOfAttendees += "
    • " + email + "
    • " @@ -44,6 +44,23 @@ func FormatBookingEmailBodyForBooker(bookingID string, roomID string, slot int, ` + appendFooter() } +// formats cancellation email body to send person who booked +func FormatCancellationEmailBodyForBooker(bookingID string, roomID string, slot int, email string) string { + + return appendHeader("Cancellation") + ` +
      +

      Dear booker,

      +

      + You have successfully cancelled your booked office space. Here are the booking details:

      + Booking ID: ` + bookingID + `
      + Room ID: ` + roomID + `
      + Slot: ` + strconv.Itoa(slot) + `

      + Thank you,
      + The Occupi Team
      +

      +
      ` + appendFooter() +} + // formats booking email body to send attendees func FormatBookingEmailBodyForAttendees(bookingID string, roomID string, slot int, email string) string { return appendHeader("Booking") + ` @@ -61,6 +78,23 @@ func FormatBookingEmailBodyForAttendees(bookingID string, roomID string, slot in ` + appendFooter() } +// formats cancellation email body to send attendees +func FormatCancellationEmailBodyForAttendees(bookingID string, roomID string, slot int, email string) string { + return appendHeader("Booking") + ` +
      +

      Dear attendees,

      +

      + ` + email + ` has cancelled the booked office space with the following details:

      + Booking ID: ` + bookingID + `
      + Room ID: ` + roomID + `
      + Slot: ` + strconv.Itoa(slot) + `

      + If you have any questions, feel free to contact us.

      + Thank you,
      + The Occupi Team
      +

      +
      ` + appendFooter() +} + // formats verification email body func FormatEmailVerificationBody(otp string, email string) string { return appendHeader("Registration") + ` @@ -115,10 +149,11 @@ func appendHeader(title string) string { } func appendFooter() string { - return ` + return ` + ` diff --git a/occupi-backend/pkg/mail/mail.go b/occupi-backend/pkg/mail/mail.go index b6bc67ac..f5192a4c 100644 --- a/occupi-backend/pkg/mail/mail.go +++ b/occupi-backend/pkg/mail/mail.go @@ -1,9 +1,11 @@ package mail import ( + "errors" "sync" "github.com/COS301-SE-2024/occupi/occupi-backend/configs" + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" "gopkg.in/gomail.v2" ) @@ -29,7 +31,7 @@ func SendMail(to string, subject string, body string) error { return nil } -func SendMultipleEmailsConcurrently(emails []string, subject, body string) []string { +func SendMultipleEmailsConcurrently(emails []string, subject, body string, creator string) []string { // Use a WaitGroup to wait for all goroutines to complete var wg sync.WaitGroup var emailErrors []string @@ -52,3 +54,65 @@ func SendMultipleEmailsConcurrently(emails []string, subject, body string) []str return emailErrors } + +func SendBookingEmails(booking models.Booking) error { + // Prepare the email content + creatorSubject := "Booking Confirmation - Occupi" + creatorBody := FormatBookingEmailBodyForBooker(booking.ID, booking.RoomID, booking.Slot, booking.Emails, booking.Creator) + + // Prepare the email content for attendees + attendeesSubject := "You're invited to a Booking - Occupi" + attendeesBody := FormatBookingEmailBodyForAttendees(booking.ID, booking.RoomID, booking.Slot, booking.Creator) + + var attendees []string + for _, email := range booking.Emails { + if email != booking.Creator { + attendees = append(attendees, email) + } + } + + creatorEmailError := SendMail(booking.Creator, creatorSubject, creatorBody) + if creatorEmailError != nil { + return creatorEmailError + } + + // Send the confirmation email concurrently to all recipients + emailErrors := SendMultipleEmailsConcurrently(attendees, attendeesSubject, attendeesBody, booking.Creator) + + if len(emailErrors) > 0 { + return errors.New("failed to send booking emails") + } + + return nil +} + +func SendCancellationEmails(booking models.Booking) error { + // Prepare the email content + creatorSubject := "Booking Cancelled - Occupi" + creatorBody := FormatCancellationEmailBodyForBooker(booking.ID, booking.RoomID, booking.Slot, booking.Creator) + + // Prepare the email content for attendees + attendeesSubject := "Booking Cancelled - Occupi" + attendeesBody := FormatCancellationEmailBodyForAttendees(booking.ID, booking.RoomID, booking.Slot, booking.Creator) + + var attendees []string + for _, email := range booking.Emails { + if email != booking.Creator { + attendees = append(attendees, email) + } + } + + creatorEmailError := SendMail(booking.Creator, creatorSubject, creatorBody) + if creatorEmailError != nil { + return creatorEmailError + } + + // Send the confirmation email concurrently to all recipients + emailErrors := SendMultipleEmailsConcurrently(attendees, attendeesSubject, attendeesBody, booking.Creator) + + if len(emailErrors) > 0 { + return errors.New("failed to send booking emails") + } + + return nil +} From f8193eee7679a6f0ea235d192606ca67dcc80947 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Wed, 26 Jun 2024 19:08:37 +0200 Subject: [PATCH 35/54] Validation implemented on one function --- occupi-backend/pkg/handlers/api_handlers.go | 19 +++++++++++++++++- occupi-backend/pkg/models/database.go | 12 +++++------ occupi-backend/pkg/models/request.go | 22 +++++++++++++++++++++ occupi-backend/pkg/utils/utils.go | 9 +++++++++ 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index 7e572636..ebef3cb1 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -8,6 +8,7 @@ import ( "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" + "github.com/go-playground/validator/v10" "go.mongodb.org/mongo-driver/bson/primitive" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/mail" @@ -55,7 +56,7 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { //link: https://cos301-se-2024.github.io/occupi/coding-standards/go-coding-standards#response-and-error-handling var booking models.Booking if err := ctx.ShouldBindJSON(&booking); err != nil { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected RoomID,Slot,Emails[],Creator,FloorNo ", nil)) + HandleValidationErrors(ctx, err) return } @@ -177,3 +178,19 @@ func ViewRooms(ctx *gin.Context, appsession *models.AppSession) { ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully fetched rooms!", rooms)) } + +// Helper function to handle validation of requests +// request validation error handler +func HandleValidationErrors(ctx *gin.Context, err error) { + var ve validator.ValidationErrors + if errors.As(err, &ve) { + out := make([]models.ErrorMsg, len(ve)) + for i, err := range ve { + out[i] = models.ErrorMsg{ + Field: utils.LowercaseFirstLetter(err.Field()), + Message: models.GetErrorMsg(err), + } + } + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Invalid request payload", gin.H{"errors": out})) + } +} diff --git a/occupi-backend/pkg/models/database.go b/occupi-backend/pkg/models/database.go index 58803367..5739c200 100644 --- a/occupi-backend/pkg/models/database.go +++ b/occupi-backend/pkg/models/database.go @@ -18,13 +18,13 @@ type User struct { type Booking struct { ID string `json:"_id" bson:"_id,omitempty"` OccupiID string `json:"occupiId" bson:"occupiId,omitempty"` - RoomID string `json:"roomId" bson:"roomId" validate:"required"` - RoomName string `json:"roomName" bson:"roomName" validate:"required"` - Slot int `json:"slot" bson:"slot" validate:"required,min=1"` - Emails []string `json:"emails" bson:"emails" validate:"required,dive,email"` + RoomID string `json:"roomId" bson:"roomId" binding:"required"` + RoomName string `json:"roomName" bson:"roomName" binding:"required"` + Slot int `json:"slot" bson:"slot" binding:"required,min=1"` + Emails []string `json:"emails" bson:"emails" binding:"required,dive,email"` CheckedIn bool `json:"checkedIn" bson:"checkedIn"` - Creator string `json:"creator" bson:"creator" validate:"required,email"` - FloorNo int `json:"floorNo" bson:"floorNo" validate:"required"` + Creator string `json:"creator" bson:"creator" binding:"required,email"` + FloorNo int `json:"floorNo" bson:"floorNo" binding:"required"` Date time.Time `json:"date" bson:"date,omitempty"` Start time.Time `json:"start" bson:"start,omitempty"` End time.Time `json:"end" bson:"end,omitempty"` diff --git a/occupi-backend/pkg/models/request.go b/occupi-backend/pkg/models/request.go index 30cc9f29..f3ff6973 100644 --- a/occupi-backend/pkg/models/request.go +++ b/occupi-backend/pkg/models/request.go @@ -1,5 +1,10 @@ package models +import ( + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils" + "github.com/go-playground/validator/v10" +) + // expected user structure from api requests type RequestUser struct { Email string `json:"email"` @@ -11,3 +16,20 @@ type RequestUserOTP struct { Email string `json:"email"` OTP string `json:"otp"` } + +type ErrorMsg struct { + Field string `json:"field"` + Message string `json:"message"` +} + +func GetErrorMsg(fe validator.FieldError) string { + switch fe.Tag() { + case "required": + return "The " + utils.LowercaseFirstLetter(fe.Field()) + " field is required" + case "email": + return "The " + fe.Field() + " field must be a valid email address" + case "min": + return "The " + fe.Field() + " field must be greater than " + fe.Param() + } + return "The " + fe.Field() + " field is invalid" +} diff --git a/occupi-backend/pkg/utils/utils.go b/occupi-backend/pkg/utils/utils.go index b119e901..71b34046 100644 --- a/occupi-backend/pkg/utils/utils.go +++ b/occupi-backend/pkg/utils/utils.go @@ -7,6 +7,7 @@ import ( "log" "os" "regexp" + "strings" "time" "github.com/alexedwards/argon2id" @@ -164,3 +165,11 @@ func CompareArgon2IDHash(password string, hashedPassword string) (bool, error) { } return match, nil } + +// Helper function to lower first case of a string +func LowercaseFirstLetter(s string) string { + if len(s) == 0 { + return s + } + return strings.ToLower(string(s[0])) + s[1:] +} From 8637efe0c9d1eca24108a1957f880d5c658e6eac Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Wed, 26 Jun 2024 19:26:44 +0200 Subject: [PATCH 36/54] Validation done --- occupi-backend/pkg/database/database.go | 4 ++-- occupi-backend/pkg/handlers/api_handlers.go | 11 +++++------ occupi-backend/pkg/mail/mail.go | 8 ++++---- occupi-backend/pkg/models/database.go | 12 ++++++------ occupi-backend/pkg/utils/response.go | 3 +-- 5 files changed, 18 insertions(+), 20 deletions(-) diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index 8ab7a37a..a5aa453b 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -134,8 +134,8 @@ func ConfirmCheckIn(ctx *gin.Context, db *mongo.Client, checkIn models.CheckIn) // Find the booking by bookingId, roomId, and creator filter := bson.M{ - "_id": checkIn.BookingID, - "roomId": checkIn.RoomID, + "_id": checkIn.BookingId, + "roomId": checkIn.RoomId, "creator": checkIn.Creator, } diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index ebef3cb1..c914681d 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -62,7 +62,7 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { // Generate a unique ID for the booking booking.ID = primitive.NewObjectID().Hex() - booking.OccupiID = utils.GenerateBookingID() + booking.OccupiId = utils.GenerateBookingID() booking.CheckedIn = false // Save the booking to the database @@ -84,7 +84,7 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { func ViewBookings(ctx *gin.Context, appsession *models.AppSession) { // Extract the email query parameter email := ctx.Query("email") - if email == "" { + if email == "" || !utils.ValidateEmail(email) { ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Email Address", nil)) return } @@ -102,7 +102,7 @@ func ViewBookings(ctx *gin.Context, appsession *models.AppSession) { // Cancel booking handles the cancellation of a booking func CancelBooking(ctx *gin.Context, appsession *models.AppSession) { var booking models.Booking - if err := ctx.ShouldBindJSON(&booking); err != nil { + if err := ctx.ShouldBindJSON(&booking); err != nil || booking.ID == "" || booking.Creator == "" { ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Booking ID, Room ID, and Email Address", nil)) return } @@ -134,12 +134,12 @@ func CheckIn(ctx *gin.Context, appsession *models.AppSession) { var checkIn models.CheckIn if err := ctx.ShouldBindJSON(&checkIn); err != nil { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Booking ID, Room ID, and Creator Email Address", nil)) + HandleValidationErrors(ctx, err) return } // Check if the booking exists - exists := database.BookingExists(ctx, appsession.DB, checkIn.BookingID) + exists := database.BookingExists(ctx, appsession.DB, checkIn.BookingId) if !exists { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to find booking", constants.InternalServerErrorCode, "Failed to find booking", nil)) return @@ -180,7 +180,6 @@ func ViewRooms(ctx *gin.Context, appsession *models.AppSession) { } // Helper function to handle validation of requests -// request validation error handler func HandleValidationErrors(ctx *gin.Context, err error) { var ve validator.ValidationErrors if errors.As(err, &ve) { diff --git a/occupi-backend/pkg/mail/mail.go b/occupi-backend/pkg/mail/mail.go index f5192a4c..1dfb150d 100644 --- a/occupi-backend/pkg/mail/mail.go +++ b/occupi-backend/pkg/mail/mail.go @@ -58,11 +58,11 @@ func SendMultipleEmailsConcurrently(emails []string, subject, body string, creat func SendBookingEmails(booking models.Booking) error { // Prepare the email content creatorSubject := "Booking Confirmation - Occupi" - creatorBody := FormatBookingEmailBodyForBooker(booking.ID, booking.RoomID, booking.Slot, booking.Emails, booking.Creator) + creatorBody := FormatBookingEmailBodyForBooker(booking.ID, booking.RoomId, booking.Slot, booking.Emails, booking.Creator) // Prepare the email content for attendees attendeesSubject := "You're invited to a Booking - Occupi" - attendeesBody := FormatBookingEmailBodyForAttendees(booking.ID, booking.RoomID, booking.Slot, booking.Creator) + attendeesBody := FormatBookingEmailBodyForAttendees(booking.ID, booking.RoomId, booking.Slot, booking.Creator) var attendees []string for _, email := range booking.Emails { @@ -89,11 +89,11 @@ func SendBookingEmails(booking models.Booking) error { func SendCancellationEmails(booking models.Booking) error { // Prepare the email content creatorSubject := "Booking Cancelled - Occupi" - creatorBody := FormatCancellationEmailBodyForBooker(booking.ID, booking.RoomID, booking.Slot, booking.Creator) + creatorBody := FormatCancellationEmailBodyForBooker(booking.ID, booking.RoomId, booking.Slot, booking.Creator) // Prepare the email content for attendees attendeesSubject := "Booking Cancelled - Occupi" - attendeesBody := FormatCancellationEmailBodyForAttendees(booking.ID, booking.RoomID, booking.Slot, booking.Creator) + attendeesBody := FormatCancellationEmailBodyForAttendees(booking.ID, booking.RoomId, booking.Slot, booking.Creator) var attendees []string for _, email := range booking.Emails { diff --git a/occupi-backend/pkg/models/database.go b/occupi-backend/pkg/models/database.go index 5739c200..ef6ae370 100644 --- a/occupi-backend/pkg/models/database.go +++ b/occupi-backend/pkg/models/database.go @@ -17,8 +17,8 @@ type User struct { // structure of booking type Booking struct { ID string `json:"_id" bson:"_id,omitempty"` - OccupiID string `json:"occupiId" bson:"occupiId,omitempty"` - RoomID string `json:"roomId" bson:"roomId" binding:"required"` + OccupiId string `json:"occupiId" bson:"occupiId,omitempty"` + RoomId string `json:"roomId" bson:"roomId" binding:"required"` RoomName string `json:"roomName" bson:"roomName" binding:"required"` Slot int `json:"slot" bson:"slot" binding:"required,min=1"` Emails []string `json:"emails" bson:"emails" binding:"required,dive,email"` @@ -32,9 +32,9 @@ type Booking struct { // structure of CheckIn type CheckIn struct { - BookingID string `json:"bookingId" bson:"bookingId"` - Creator string `json:"creator" bson:"creator"` - RoomID string `json:"roomId" bson:"roomId"` + BookingId string `json:"bookingId" bson:"bookingId binding:"required"` + Creator string `json:"creator" bson:"creator" binding:"required,email"` + RoomId string `json:"roomId" bson:"roomId" binding:"required"` } type OTP struct { @@ -50,7 +50,7 @@ type ViewBookings struct { type Room struct { ID string `json:"_id" bson:"_id,omitempty"` - RoomID string `json:"roomId" bson:"roomId,omitempty"` + RoomId string `json:"roomId" bson:"roomId,omitempty"` RoomNo int `json:"roomNo" bson:"roomNo,omitempty"` FloorNo int `json:"floorNo" bson:"floorNo,omitempty"` MinOccupancy int `json:"minOccupancy" bson:"minOccupancy,omitempty"` diff --git a/occupi-backend/pkg/utils/response.go b/occupi-backend/pkg/utils/response.go index 263940d5..5e27bc67 100644 --- a/occupi-backend/pkg/utils/response.go +++ b/occupi-backend/pkg/utils/response.go @@ -3,9 +3,8 @@ package utils import ( "net/http" - "github.com/gin-gonic/gin" - "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" + "github.com/gin-gonic/gin" ) // creates success response and formats it correctly From 8c16a4b7d6ca9677037b9c45299d1ca03d5fbbbf Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Wed, 26 Jun 2024 23:11:58 +0200 Subject: [PATCH 37/54] Linting errors should be fixed --- occupi-backend/pkg/database/database.go | 4 ++-- occupi-backend/pkg/handlers/api_handlers.go | 4 ++-- occupi-backend/pkg/models/database.go | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index a5aa453b..8ab7a37a 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -134,8 +134,8 @@ func ConfirmCheckIn(ctx *gin.Context, db *mongo.Client, checkIn models.CheckIn) // Find the booking by bookingId, roomId, and creator filter := bson.M{ - "_id": checkIn.BookingId, - "roomId": checkIn.RoomId, + "_id": checkIn.BookingID, + "roomId": checkIn.RoomID, "creator": checkIn.Creator, } diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index c914681d..869523e1 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -62,7 +62,7 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { // Generate a unique ID for the booking booking.ID = primitive.NewObjectID().Hex() - booking.OccupiId = utils.GenerateBookingID() + booking.OccupiID = utils.GenerateBookingID() booking.CheckedIn = false // Save the booking to the database @@ -139,7 +139,7 @@ func CheckIn(ctx *gin.Context, appsession *models.AppSession) { } // Check if the booking exists - exists := database.BookingExists(ctx, appsession.DB, checkIn.BookingId) + exists := database.BookingExists(ctx, appsession.DB, checkIn.BookingID) if !exists { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to find booking", constants.InternalServerErrorCode, "Failed to find booking", nil)) return diff --git a/occupi-backend/pkg/models/database.go b/occupi-backend/pkg/models/database.go index ef6ae370..520c4610 100644 --- a/occupi-backend/pkg/models/database.go +++ b/occupi-backend/pkg/models/database.go @@ -17,7 +17,7 @@ type User struct { // structure of booking type Booking struct { ID string `json:"_id" bson:"_id,omitempty"` - OccupiId string `json:"occupiId" bson:"occupiId,omitempty"` + OccupiID string `json:"occupiId" bson:"occupiId,omitempty"` RoomId string `json:"roomId" bson:"roomId" binding:"required"` RoomName string `json:"roomName" bson:"roomName" binding:"required"` Slot int `json:"slot" bson:"slot" binding:"required,min=1"` @@ -32,9 +32,9 @@ type Booking struct { // structure of CheckIn type CheckIn struct { - BookingId string `json:"bookingId" bson:"bookingId binding:"required"` + BookingID string `json:"bookingId" bson:"bookingId" binding:"required"` Creator string `json:"creator" bson:"creator" binding:"required,email"` - RoomId string `json:"roomId" bson:"roomId" binding:"required"` + RoomID string `json:"roomId" bson:"roomId" binding:"required"` } type OTP struct { From 4998ad8e63beb1a1183855d21bb951fcfbf74372 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Wed, 26 Jun 2024 23:17:03 +0200 Subject: [PATCH 38/54] All errors should be fixed --- occupi-backend/pkg/handlers/api_handlers.go | 2 +- occupi-backend/pkg/models/database.go | 2 +- occupi-backend/tests/utils_test.go | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index 869523e1..b4ab3822 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -122,7 +122,7 @@ func CancelBooking(ctx *gin.Context, appsession *models.AppSession) { } if err := mail.SendCancellationEmails(booking); err != nil { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "An error occured", constants.InternalServerErrorCode, "Failed to send booking email", nil)) + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "An error occurred", constants.InternalServerErrorCode, "Failed to send booking email", nil)) return } diff --git a/occupi-backend/pkg/models/database.go b/occupi-backend/pkg/models/database.go index 520c4610..8eb3e1ef 100644 --- a/occupi-backend/pkg/models/database.go +++ b/occupi-backend/pkg/models/database.go @@ -50,7 +50,7 @@ type ViewBookings struct { type Room struct { ID string `json:"_id" bson:"_id,omitempty"` - RoomId string `json:"roomId" bson:"roomId,omitempty"` + RoomID string `json:"roomId" bson:"roomId,omitempty"` RoomNo int `json:"roomNo" bson:"roomNo,omitempty"` FloorNo int `json:"floorNo" bson:"floorNo,omitempty"` MinOccupancy int `json:"minOccupancy" bson:"minOccupancy,omitempty"` diff --git a/occupi-backend/tests/utils_test.go b/occupi-backend/tests/utils_test.go index 6cc213ad..82a1a087 100644 --- a/occupi-backend/tests/utils_test.go +++ b/occupi-backend/tests/utils_test.go @@ -400,3 +400,23 @@ func TestGenerateOTP(t *testing.T) { t.Logf("Generated OTP: %s", otp) } +func TestLowercaseFirstLetter(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"Hello", "hello"}, + {"world", "world"}, + {"Golang", "golang"}, + {"", ""}, + {"A", "a"}, + {"ABC", "aBC"}, + } + + for _, test := range tests { + result := utils.LowercaseFirstLetter(test.input) + if result != test.expected { + t.Errorf("LowercaseFirstLetter(%q) = %q; expected %q", test.input, result, test.expected) + } + } +} From f5f5f414733db286449ee65954b81f333142a2b6 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Fri, 28 Jun 2024 02:18:25 +0200 Subject: [PATCH 39/54] Validation has changed a lot need to update docs heavy --- occupi-backend/pkg/database/database.go | 19 ++---- occupi-backend/pkg/handlers/api_handlers.go | 69 +++++++++++++-------- occupi-backend/pkg/mail/mail.go | 20 +++--- occupi-backend/pkg/models/database.go | 36 +++++++---- occupi-backend/pkg/utils/utils.go | 49 +++++++++++++++ occupi-backend/tests/handlers_test.go | 32 ++++------ occupi-backend/tests/utils_test.go | 32 ++++++++++ 7 files changed, 178 insertions(+), 79 deletions(-) diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index 8ab7a37a..ca3a8f0d 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -370,8 +370,8 @@ func ConfirmCancellation(ctx *gin.Context, db *mongo.Client, id string, email st } // Gets all rooms available for booking -func GetAllRooms(ctx *gin.Context, db *mongo.Client, floorNo int) ([]models.Room, error) { - collection := db.Database("Occupi").Collection("Rooms") +func GetAllRooms(ctx *gin.Context, db *mongo.Client, floorNo string) ([]models.Room, error) { + collection := db.Database("Occupi").Collection("RoomsV2") var cursor *mongo.Cursor var err error @@ -380,17 +380,10 @@ func GetAllRooms(ctx *gin.Context, db *mongo.Client, floorNo int) ([]models.Room // findOptions.SetLimit(10) // Limit the results to 10 // findOptions.SetSkip(int64(10)) // Skip the specified number of documents for pagination - if floorNo == 0 { - // Find all rooms - filter := bson.M{"floorNo": 0} - // cursor, err = collection.Find(context.TODO(), filter, findOptions) - cursor, err = collection.Find(context.TODO(), filter) - } else { - // Find all rooms on the specified floor - filter := bson.M{"floorNo": floorNo} - // cursor, err = collection.Find(context.TODO(), filter, findOptions) - cursor, err = collection.Find(context.TODO(), filter) - } + // Find all rooms on the specified floor + filter := bson.M{"floorNo": floorNo} + // cursor, err = collection.Find(context.TODO(), filter, findOptions) + cursor, err = collection.Find(context.TODO(), filter) if err != nil { logrus.Error(err) diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index b4ab3822..0334f06c 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -1,9 +1,11 @@ package handlers import ( + "encoding/json" "errors" "io" "net/http" + "reflect" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database" @@ -55,27 +57,32 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { // consider structuring api responses to match that as outlined in our coding standards documentation //link: https://cos301-se-2024.github.io/occupi/coding-standards/go-coding-standards#response-and-error-handling var booking models.Booking + if err := ctx.ShouldBindJSON(&booking); err != nil { HandleValidationErrors(ctx, err) return } + if booking.RoomID == "" || booking.RoomName == "" || booking.Creator == "" || len(booking.Emails) == 0 { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Invalid inputs", nil)) + return + } // Generate a unique ID for the booking booking.ID = primitive.NewObjectID().Hex() booking.OccupiID = utils.GenerateBookingID() booking.CheckedIn = false - // Save the booking to the database - _, err := database.SaveBooking(ctx, appsession.DB, booking) - if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to save booking", constants.InternalServerErrorCode, "Failed to save booking", nil)) - return - } + // // Save the booking to the database + // _, err := database.SaveBooking(ctx, appsession.DB, booking) + // if err != nil { + // ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to save booking", constants.InternalServerErrorCode, "Failed to save booking", nil)) + // return + // } - if err := mail.SendBookingEmails(booking); err != nil { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send booking email", constants.InternalServerErrorCode, "Failed to send booking email", nil)) - return - } + // if err := mail.SendBookingEmails(booking); err != nil { + // ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send booking email", constants.InternalServerErrorCode, "Failed to send booking email", nil)) + // return + // } ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully booked!", booking.ID)) } @@ -101,27 +108,27 @@ func ViewBookings(ctx *gin.Context, appsession *models.AppSession) { // Cancel booking handles the cancellation of a booking func CancelBooking(ctx *gin.Context, appsession *models.AppSession) { - var booking models.Booking - if err := ctx.ShouldBindJSON(&booking); err != nil || booking.ID == "" || booking.Creator == "" { + var cancel models.Cancel + if err := ctx.ShouldBindJSON(&cancel); err != nil || cancel.ID == "" || cancel.Creator == "" { ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Booking ID, Room ID, and Email Address", nil)) return } // Check if the booking exists - exists := database.BookingExists(ctx, appsession.DB, booking.ID) + exists := database.BookingExists(ctx, appsession.DB, cancel.ID) if !exists { ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(404, "Booking not found", constants.InternalServerErrorCode, "Booking not found", nil)) return } // Confirm the cancellation to the database - _, err := database.ConfirmCancellation(ctx, appsession.DB, booking.ID, booking.Creator) + _, err := database.ConfirmCancellation(ctx, appsession.DB, cancel.ID, cancel.Creator) if err != nil { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to cancel booking", constants.InternalServerErrorCode, "Failed to cancel booking", nil)) return } - if err := mail.SendCancellationEmails(booking); err != nil { + if err := mail.SendCancellationEmails(cancel); err != nil { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "An error occurred", constants.InternalServerErrorCode, "Failed to send booking email", nil)) return } @@ -156,21 +163,33 @@ func CheckIn(ctx *gin.Context, appsession *models.AppSession) { // View all available rooms func ViewRooms(ctx *gin.Context, appsession *models.AppSession) { - var room models.Room - if err := ctx.ShouldBindJSON(&room); err != nil && !errors.Is(err, io.EOF) { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Floor No", nil)) + var roomRequest map[string]interface{} + var room models.RoomRequest + if err := ctx.ShouldBindJSON(&roomRequest); err != nil && !errors.Is(err, io.EOF) { + HandleValidationErrors(ctx, err) return } - var rooms []models.Room - var err error - if room.FloorNo != -1 { - // If FloorNo is provided, filter by FloorNo - rooms, err = database.GetAllRooms(ctx, appsession.DB, room.FloorNo) + + // Validate JSON + if err := utils.ValidateJSON(roomRequest, reflect.TypeOf(models.RoomRequest{})); err != nil { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, err.Error(), constants.BadRequestCode, err.Error(), nil)) + return + } + + // Convert validated JSON to RoomRequest struct + roomBytes, _ := json.Marshal(roomRequest) + json.Unmarshal(roomBytes, &room) + + var floorNo string + if room.FloorNo == "" { + floorNo = "0" } else { - // If FloorNo is not provided, fetch all rooms on the ground floor - rooms, err = database.GetAllRooms(ctx, appsession.DB, 0) + floorNo = room.FloorNo } + var err error + var rooms []models.Room + rooms, err = database.GetAllRooms(ctx, appsession.DB, floorNo) if err != nil { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to get rooms", constants.InternalServerErrorCode, "Failed to get rooms", nil)) return diff --git a/occupi-backend/pkg/mail/mail.go b/occupi-backend/pkg/mail/mail.go index 1dfb150d..80421864 100644 --- a/occupi-backend/pkg/mail/mail.go +++ b/occupi-backend/pkg/mail/mail.go @@ -58,11 +58,11 @@ func SendMultipleEmailsConcurrently(emails []string, subject, body string, creat func SendBookingEmails(booking models.Booking) error { // Prepare the email content creatorSubject := "Booking Confirmation - Occupi" - creatorBody := FormatBookingEmailBodyForBooker(booking.ID, booking.RoomId, booking.Slot, booking.Emails, booking.Creator) + creatorBody := FormatBookingEmailBodyForBooker(booking.ID, booking.RoomID, 0, booking.Emails, booking.Creator) // Prepare the email content for attendees attendeesSubject := "You're invited to a Booking - Occupi" - attendeesBody := FormatBookingEmailBodyForAttendees(booking.ID, booking.RoomId, booking.Slot, booking.Creator) + attendeesBody := FormatBookingEmailBodyForAttendees(booking.ID, booking.RoomID, 0, booking.Creator) var attendees []string for _, email := range booking.Emails { @@ -86,32 +86,32 @@ func SendBookingEmails(booking models.Booking) error { return nil } -func SendCancellationEmails(booking models.Booking) error { +func SendCancellationEmails(cancel models.Cancel) error { // Prepare the email content creatorSubject := "Booking Cancelled - Occupi" - creatorBody := FormatCancellationEmailBodyForBooker(booking.ID, booking.RoomId, booking.Slot, booking.Creator) + creatorBody := FormatCancellationEmailBodyForBooker(cancel.ID, cancel.RoomID, 0, cancel.Creator) // Prepare the email content for attendees attendeesSubject := "Booking Cancelled - Occupi" - attendeesBody := FormatCancellationEmailBodyForAttendees(booking.ID, booking.RoomId, booking.Slot, booking.Creator) + attendeesBody := FormatCancellationEmailBodyForAttendees(cancel.ID, cancel.RoomID, 0, cancel.Creator) var attendees []string - for _, email := range booking.Emails { - if email != booking.Creator { + for _, email := range cancel.Emails { + if email != cancel.Creator { attendees = append(attendees, email) } } - creatorEmailError := SendMail(booking.Creator, creatorSubject, creatorBody) + creatorEmailError := SendMail(cancel.Creator, creatorSubject, creatorBody) if creatorEmailError != nil { return creatorEmailError } // Send the confirmation email concurrently to all recipients - emailErrors := SendMultipleEmailsConcurrently(attendees, attendeesSubject, attendeesBody, booking.Creator) + emailErrors := SendMultipleEmailsConcurrently(attendees, attendeesSubject, attendeesBody, cancel.Creator) if len(emailErrors) > 0 { - return errors.New("failed to send booking emails") + return errors.New("failed to send cancellation emails") } return nil diff --git a/occupi-backend/pkg/models/database.go b/occupi-backend/pkg/models/database.go index 8eb3e1ef..8e2a2f64 100644 --- a/occupi-backend/pkg/models/database.go +++ b/occupi-backend/pkg/models/database.go @@ -18,16 +18,26 @@ type User struct { type Booking struct { ID string `json:"_id" bson:"_id,omitempty"` OccupiID string `json:"occupiId" bson:"occupiId,omitempty"` - RoomId string `json:"roomId" bson:"roomId" binding:"required"` + RoomID string `json:"roomId" bson:"roomId" binding:"required"` RoomName string `json:"roomName" bson:"roomName" binding:"required"` - Slot int `json:"slot" bson:"slot" binding:"required,min=1"` Emails []string `json:"emails" bson:"emails" binding:"required,dive,email"` CheckedIn bool `json:"checkedIn" bson:"checkedIn"` Creator string `json:"creator" bson:"creator" binding:"required,email"` - FloorNo int `json:"floorNo" bson:"floorNo" binding:"required"` - Date time.Time `json:"date" bson:"date,omitempty"` - Start time.Time `json:"start" bson:"start,omitempty"` - End time.Time `json:"end" bson:"end,omitempty"` + FloorNo string `json:"floorNo" bson:"floorNo" binding:"required"` + Date time.Time `json:"date" bson:"date" binding:"required"` + Start time.Time `json:"start" bson:"start" binding:"required"` + End time.Time `json:"end" bson:"end" binding:"required"` +} +type Cancel struct { + ID string `json:"_id" bson:"_id,omitempty"` + RoomID string `json:"roomId" bson:"roomId" binding:"required"` + RoomName string `json:"roomName" bson:"roomName" binding:"required"` + Emails []string `json:"emails" bson:"emails" binding:"required,dive,email"` + Creator string `json:"creator" bson:"creator" binding:"required,email"` + FloorNo string `json:"floorNo" bson:"floorNo" binding:"required"` + Date time.Time `json:"date" bson:"date" binding:"required"` + Start time.Time `json:"start" bson:"start" binding:"required"` + End time.Time `json:"end" bson:"end" binding:"required"` } // structure of CheckIn @@ -51,10 +61,14 @@ type ViewBookings struct { type Room struct { ID string `json:"_id" bson:"_id,omitempty"` RoomID string `json:"roomId" bson:"roomId,omitempty"` - RoomNo int `json:"roomNo" bson:"roomNo,omitempty"` - FloorNo int `json:"floorNo" bson:"floorNo,omitempty"` + RoomNo string `json:"roomNo" bson:"roomNo,omitempty"` + FloorNo string `json:"floorNo" bson:"floorNo" binding:"required"` MinOccupancy int `json:"minOccupancy" bson:"minOccupancy,omitempty"` - MaxOccupancy int `json:"maxOccupancy" bson:"maxOccupancy,omitempty"` - Description string `json:"description" bson:"description,omitempty"` - RoomName string `json:"roomName" bson:"roomName,omitempty"` + MaxOccupancy int `json:"maxOccupancy" bson:"maxOccupancy"` + Description string `json:"description" bson:"description"` + RoomName string `json:"roomName" bson:"roomName"` +} + +type RoomRequest struct { + FloorNo string `json:"floorNo" bson:"floorNo" binding:"required"` } diff --git a/occupi-backend/pkg/utils/utils.go b/occupi-backend/pkg/utils/utils.go index 71b34046..59e7ee24 100644 --- a/occupi-backend/pkg/utils/utils.go +++ b/occupi-backend/pkg/utils/utils.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "os" + "reflect" "regexp" "strings" "time" @@ -173,3 +174,51 @@ func LowercaseFirstLetter(s string) string { } return strings.ToLower(string(s[0])) + s[1:] } + +func TypeCheck(value interface{}, expectedType reflect.Type) bool { + valueType := reflect.TypeOf(value) + + // Handle pointer types by dereferencing + if expectedType.Kind() == reflect.Ptr { + expectedType = expectedType.Elem() + if value == nil { + return true + } + } + + // Handle pointer types by dereferencing + if valueType != nil && valueType.Kind() == reflect.Ptr { + valueType = valueType.Elem() + } + + if expectedType.Kind() == reflect.Ptr { + expectedType = expectedType.Elem() + } + + return valueType == expectedType +} + +func ValidateJSON(data map[string]interface{}, expectedType reflect.Type) error { + for i := 0; i < expectedType.NumField(); i++ { + field := expectedType.Field(i) + jsonTag := field.Tag.Get("json") + validateTag := field.Tag.Get("binding") + + // Check if the JSON field exists + value, exists := data[jsonTag] + if !exists { + if validateTag == "required" { + logrus.Error("missing required field: ", jsonTag) + return fmt.Errorf("missing required field: %s", jsonTag) + } + continue + } + + // Check the field type + if !TypeCheck(value, field.Type) { + logrus.Error("field ", jsonTag, " is of incorrect type") + return fmt.Errorf("field %s is of incorrect type", jsonTag) + } + } + return nil +} diff --git a/occupi-backend/tests/handlers_test.go b/occupi-backend/tests/handlers_test.go index 30dbb777..d1827cd3 100644 --- a/occupi-backend/tests/handlers_test.go +++ b/occupi-backend/tests/handlers_test.go @@ -133,8 +133,10 @@ func TestBookRoom(t *testing.T) { // Register the route router.OccupiRouter(r, db) + // Generate a token token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) + // Ping-auth test to ensure everything is set up correctly w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/ping-auth", nil) req.AddCookie(&http.Cookie{Name: "token", Value: token}) @@ -157,25 +159,24 @@ func TestBookRoom(t *testing.T) { payload string expectedStatusCode int expectedMessage string - expectedData gin.H }{ { name: "Valid Request", payload: `{ - "roomId": "12345", + "roomID": "12345", "Slot": 1, "Emails": ["test@example.com"], "Creator": "test@example.com", - "FloorNo": 1 + "FloorNo": 1, + "roomName": "Test Room" }`, expectedStatusCode: http.StatusOK, expectedMessage: "Successfully booked!", - expectedData: gin.H{"id": "some_generated_id"}, // The exact value will be replaced dynamically }, { name: "Invalid Request Payload", payload: `{ - "RoomID": "", + "roomID": "", "Slot": "", "Emails": [], "Creator": "", @@ -183,14 +184,13 @@ func TestBookRoom(t *testing.T) { }`, expectedStatusCode: http.StatusBadRequest, expectedMessage: "Invalid request payload", - expectedData: nil, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Create a request to pass to the handler - req, err := http.NewRequest("POST", "/api/book-room", strings.NewReader(tc.payload)) + req, err := http.NewRequest("POST", "/api/book-room", bytes.NewBuffer([]byte(tc.payload))) if err != nil { t.Fatal(err) } @@ -209,22 +209,14 @@ func TestBookRoom(t *testing.T) { // Check the status code is what we expect assert.Equal(t, tc.expectedStatusCode, rr.Code, "handler returned wrong status code") - // Define the expected response - expectedResponse := gin.H{ - "message": tc.expectedMessage, - "status": float64(tc.expectedStatusCode), - "data": tc.expectedData, - } - - // Unmarshal the actual response - var actualResponse gin.H - if err := json.Unmarshal(rr.Body.Bytes(), &actualResponse); err != nil { + // Check the response message + var actualResponse map[string]interface{} + err = json.Unmarshal(rr.Body.Bytes(), &actualResponse) + if err != nil { t.Fatalf("could not unmarshal response: %v", err) } - // Check the response message and status - assert.Equal(t, expectedResponse["message"], actualResponse["message"], "handler returned unexpected message") - assert.Equal(t, expectedResponse["status"], actualResponse["status"], "handler returned unexpected status") + assert.Equal(t, tc.expectedMessage, actualResponse["message"], "handler returned unexpected message") // For successful booking, check if the ID is generated if tc.expectedStatusCode == http.StatusOK { diff --git a/occupi-backend/tests/utils_test.go b/occupi-backend/tests/utils_test.go index 82a1a087..f0690640 100644 --- a/occupi-backend/tests/utils_test.go +++ b/occupi-backend/tests/utils_test.go @@ -2,6 +2,7 @@ package tests import ( "net/http" + "reflect" "testing" "github.com/gin-gonic/gin" @@ -420,3 +421,34 @@ func TestLowercaseFirstLetter(t *testing.T) { } } } + +func TestTypeCheck(t *testing.T) { + tests := []struct { + name string + value interface{} + expectedType reflect.Type + expected bool + }{ + {"Match int", 42, reflect.TypeOf(42), true}, + {"Match string", "hello", reflect.TypeOf("hello"), true}, + {"Match float", 3.14, reflect.TypeOf(3.14), true}, + {"Mismatch int", "42", reflect.TypeOf(42), false}, + {"Mismatch string", 42, reflect.TypeOf("hello"), false}, + {"Pointer match", new(int), reflect.TypeOf(new(int)), true}, + {"Pointer mismatch", new(string), reflect.TypeOf(new(int)), false}, + {"Nil pointer", nil, reflect.TypeOf((*int)(nil)), true}, + {"Non-nil pointer match", new(int), reflect.TypeOf((*int)(nil)), true}, + {"Nil non-pointer", nil, reflect.TypeOf(42), false}, + {"Pointer to int", new(int), reflect.TypeOf(int(0)), true}, + {"Pointer to string", new(string), reflect.TypeOf(""), true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := utils.TypeCheck(tt.value, tt.expectedType) + if result != tt.expected { + t.Errorf("typeCheck(%v, %v) = %v; want %v", tt.value, tt.expectedType, result, tt.expected) + } + }) + } +} From a2afee9a84945740147b61e2f5a2f452d4b152af Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Fri, 28 Jun 2024 02:28:22 +0200 Subject: [PATCH 40/54] "Utils unit tests updated" --- occupi-backend/tests/utils_test.go | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/occupi-backend/tests/utils_test.go b/occupi-backend/tests/utils_test.go index f0690640..8bc42ea3 100644 --- a/occupi-backend/tests/utils_test.go +++ b/occupi-backend/tests/utils_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" + "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils" ) @@ -422,6 +423,53 @@ func TestLowercaseFirstLetter(t *testing.T) { } } +func TestValidateJSON(t *testing.T) { + tests := []struct { + name string + data map[string]interface{} + expectedType reflect.Type + expectError bool + errorMessage string + }{ + { + name: "Valid JSON", + data: map[string]interface{}{ + "floorNo": "1", + }, + expectedType: reflect.TypeOf(models.RoomRequest{}), + expectError: false, + }, + { + name: "Missing required field", + data: map[string]interface{}{}, + expectedType: reflect.TypeOf(models.RoomRequest{}), + expectError: true, + errorMessage: "missing required field: floorNo", + }, + { + name: "Incorrect type", + data: map[string]interface{}{ + "floorNo": 123, + }, + expectedType: reflect.TypeOf(models.RoomRequest{}), + expectError: true, + errorMessage: "field floorNo is of incorrect type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := utils.ValidateJSON(tt.data, tt.expectedType) + if (err != nil) != tt.expectError { + t.Errorf("ValidateJSON() error = %v, expectError %v", err, tt.expectError) + } + if tt.expectError && err.Error() != tt.errorMessage { + t.Errorf("ValidateJSON() error = %v, errorMessage %v", err.Error(), tt.errorMessage) + } + }) + } +} + func TestTypeCheck(t *testing.T) { tests := []struct { name string From eff2e9da418cb7f1ca5053d52fb9381e5a084153 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Fri, 28 Jun 2024 03:53:27 +0200 Subject: [PATCH 41/54] "Updated unit tests" --- occupi-backend/pkg/utils/utils.go | 47 +++++++++++++++++++++++++----- occupi-backend/tests/utils_test.go | 44 ++++++++++++++++++---------- 2 files changed, 68 insertions(+), 23 deletions(-) diff --git a/occupi-backend/pkg/utils/utils.go b/occupi-backend/pkg/utils/utils.go index 59e7ee24..c3452bb1 100644 --- a/occupi-backend/pkg/utils/utils.go +++ b/occupi-backend/pkg/utils/utils.go @@ -186,19 +186,40 @@ func TypeCheck(value interface{}, expectedType reflect.Type) bool { } } - // Handle pointer types by dereferencing if valueType != nil && valueType.Kind() == reflect.Ptr { valueType = valueType.Elem() } - if expectedType.Kind() == reflect.Ptr { - expectedType = expectedType.Elem() + // Handle slices and arrays + if expectedType.Kind() == reflect.Slice || expectedType.Kind() == reflect.Array { + if valueType.Kind() != reflect.Slice && valueType.Kind() != reflect.Array { + return false + } + elemType := expectedType.Elem() + for i := 0; i < reflect.ValueOf(value).Len(); i++ { + if !TypeCheck(reflect.ValueOf(value).Index(i).Interface(), elemType) { + return false + } + } + return true + } + + // Handle time.Time type + if expectedType == reflect.TypeOf(time.Time{}) { + _, ok := value.(string) + if !ok { + return false + } + _, err := time.Parse(time.RFC3339, value.(string)) + return err == nil } return valueType == expectedType } -func ValidateJSON(data map[string]interface{}, expectedType reflect.Type) error { +func ValidateJSON(data map[string]interface{}, expectedType reflect.Type) (map[string]interface{}, error) { + validatedData := make(map[string]interface{}) + for i := 0; i < expectedType.NumField(); i++ { field := expectedType.Field(i) jsonTag := field.Tag.Get("json") @@ -209,7 +230,7 @@ func ValidateJSON(data map[string]interface{}, expectedType reflect.Type) error if !exists { if validateTag == "required" { logrus.Error("missing required field: ", jsonTag) - return fmt.Errorf("missing required field: %s", jsonTag) + return nil, fmt.Errorf("missing required field: %s", jsonTag) } continue } @@ -217,8 +238,20 @@ func ValidateJSON(data map[string]interface{}, expectedType reflect.Type) error // Check the field type if !TypeCheck(value, field.Type) { logrus.Error("field ", jsonTag, " is of incorrect type") - return fmt.Errorf("field %s is of incorrect type", jsonTag) + return nil, fmt.Errorf("field %s is of incorrect type", jsonTag) + } + + // Parse date/time strings to time.Time + if field.Type == reflect.TypeOf(time.Time{}) { + parsedTime, err := time.Parse(time.RFC3339, value.(string)) + if err != nil { + logrus.Error("field ", jsonTag, " is of incorrect format") + return nil, fmt.Errorf("field %s is of incorrect format", jsonTag) + } + validatedData[jsonTag] = parsedTime + } else { + validatedData[jsonTag] = value } } - return nil + return validatedData, nil } diff --git a/occupi-backend/tests/utils_test.go b/occupi-backend/tests/utils_test.go index 8bc42ea3..44fc2d8a 100644 --- a/occupi-backend/tests/utils_test.go +++ b/occupi-backend/tests/utils_test.go @@ -4,12 +4,12 @@ import ( "net/http" "reflect" "testing" + "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" - "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils" ) @@ -423,6 +423,12 @@ func TestLowercaseFirstLetter(t *testing.T) { } } +type SampleStruct struct { + Field1 string `json:"field1" binding:"required"` + Field2 int `json:"field2" binding:"required"` + Field3 time.Time `json:"field3" binding:"required"` +} + func TestValidateJSON(t *testing.T) { tests := []struct { name string @@ -432,34 +438,41 @@ func TestValidateJSON(t *testing.T) { errorMessage string }{ { - name: "Valid JSON", + name: "Valid JSON with required fields", data: map[string]interface{}{ - "floorNo": "1", + "field1": "value1", + "field2": 123, + "field3": "2024-07-01T09:00:00Z", }, - expectedType: reflect.TypeOf(models.RoomRequest{}), + expectedType: reflect.TypeOf(SampleStruct{}), expectError: false, }, { - name: "Missing required field", - data: map[string]interface{}{}, - expectedType: reflect.TypeOf(models.RoomRequest{}), + name: "Missing required field", + data: map[string]interface{}{ + "field2": 123, + "field3": "2024-07-01T09:00:00Z", + }, + expectedType: reflect.TypeOf(SampleStruct{}), expectError: true, - errorMessage: "missing required field: floorNo", + errorMessage: "missing required field: field1", }, { - name: "Incorrect type", + name: "Invalid time format", data: map[string]interface{}{ - "floorNo": 123, + "field1": "value1", + "field2": 123, + "field3": "not-a-date", }, - expectedType: reflect.TypeOf(models.RoomRequest{}), + expectedType: reflect.TypeOf(SampleStruct{}), expectError: true, - errorMessage: "field floorNo is of incorrect type", + errorMessage: "field field3 is of incorrect type", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := utils.ValidateJSON(tt.data, tt.expectedType) + _, err := utils.ValidateJSON(tt.data, tt.expectedType) if (err != nil) != tt.expectError { t.Errorf("ValidateJSON() error = %v, expectError %v", err, tt.expectError) } @@ -487,15 +500,14 @@ func TestTypeCheck(t *testing.T) { {"Nil pointer", nil, reflect.TypeOf((*int)(nil)), true}, {"Non-nil pointer match", new(int), reflect.TypeOf((*int)(nil)), true}, {"Nil non-pointer", nil, reflect.TypeOf(42), false}, - {"Pointer to int", new(int), reflect.TypeOf(int(0)), true}, - {"Pointer to string", new(string), reflect.TypeOf(""), true}, + {"Time type valid RFC3339", "2024-07-01T09:00:00Z", reflect.TypeOf(time.Time{}), true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := utils.TypeCheck(tt.value, tt.expectedType) if result != tt.expected { - t.Errorf("typeCheck(%v, %v) = %v; want %v", tt.value, tt.expectedType, result, tt.expected) + t.Errorf("TypeCheck(%v, %v) = %v; want %v", tt.value, tt.expectedType, result, tt.expected) } }) } From f333c8b208b941bf761448c472809b1698be9bb5 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Fri, 28 Jun 2024 03:58:58 +0200 Subject: [PATCH 42/54] "Commented out merge conflicts" --- occupi-backend/pkg/handlers/api_handlers.go | 89 ++-- occupi-backend/tests/authenticator_test.go | 1 - occupi-backend/tests/handlers_test.go | 423 ++++++++++---------- 3 files changed, 276 insertions(+), 237 deletions(-) diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index 0334f06c..419139cd 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -54,35 +54,40 @@ func FetchResourceAuth(ctx *gin.Context, appsession *models.AppSession) { // BookRoom handles booking a room and sends a confirmation email func BookRoom(ctx *gin.Context, appsession *models.AppSession) { - // consider structuring api responses to match that as outlined in our coding standards documentation - //link: https://cos301-se-2024.github.io/occupi/coding-standards/go-coding-standards#response-and-error-handling - var booking models.Booking - - if err := ctx.ShouldBindJSON(&booking); err != nil { + var bookingRequest map[string]interface{} + if err := ctx.ShouldBindJSON(&bookingRequest); err != nil { HandleValidationErrors(ctx, err) return } - if booking.RoomID == "" || booking.RoomName == "" || booking.Creator == "" || len(booking.Emails) == 0 { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Invalid inputs", nil)) + + // Validate JSON + validatedData, err := utils.ValidateJSON(bookingRequest, reflect.TypeOf(models.Booking{})) + if err != nil { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, err.Error(), constants.BadRequestCode, err.Error(), nil)) return } + // Convert validated data to Booking struct + var booking models.Booking + bookingBytes, _ := json.Marshal(validatedData) + json.Unmarshal(bookingBytes, &booking) + // Generate a unique ID for the booking booking.ID = primitive.NewObjectID().Hex() booking.OccupiID = utils.GenerateBookingID() booking.CheckedIn = false - // // Save the booking to the database - // _, err := database.SaveBooking(ctx, appsession.DB, booking) - // if err != nil { - // ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to save booking", constants.InternalServerErrorCode, "Failed to save booking", nil)) - // return - // } + // Save the booking to the database + _, err = database.SaveBooking(ctx, appsession.DB, booking) + if err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to save booking", constants.InternalServerErrorCode, "Failed to save booking", nil)) + return + } - // if err := mail.SendBookingEmails(booking); err != nil { - // ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send booking email", constants.InternalServerErrorCode, "Failed to send booking email", nil)) - // return - // } + if err := mail.SendBookingEmails(booking); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send booking email", constants.InternalServerErrorCode, "Failed to send booking email", nil)) + return + } ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully booked!", booking.ID)) } @@ -106,23 +111,34 @@ func ViewBookings(ctx *gin.Context, appsession *models.AppSession) { ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully fetched bookings!", bookings)) } -// Cancel booking handles the cancellation of a booking func CancelBooking(ctx *gin.Context, appsession *models.AppSession) { - var cancel models.Cancel - if err := ctx.ShouldBindJSON(&cancel); err != nil || cancel.ID == "" || cancel.Creator == "" { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Booking ID, Room ID, and Email Address", nil)) + var cancelRequest map[string]interface{} + if err := ctx.ShouldBindJSON(&cancelRequest); err != nil { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Invalid JSON payload", nil)) + return + } + + // Validate JSON + validatedData, err := utils.ValidateJSON(cancelRequest, reflect.TypeOf(models.Cancel{})) + if err != nil { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, err.Error(), constants.BadRequestCode, err.Error(), nil)) return } + // Convert validated JSON to Cancel struct + var cancel models.Cancel + cancelBytes, _ := json.Marshal(validatedData) + json.Unmarshal(cancelBytes, &cancel) + // Check if the booking exists exists := database.BookingExists(ctx, appsession.DB, cancel.ID) if !exists { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(404, "Booking not found", constants.InternalServerErrorCode, "Booking not found", nil)) + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusNotFound, "Booking not found", constants.InternalServerErrorCode, "Booking not found", nil)) return } // Confirm the cancellation to the database - _, err := database.ConfirmCancellation(ctx, appsession.DB, cancel.ID, cancel.Creator) + _, err = database.ConfirmCancellation(ctx, appsession.DB, cancel.ID, cancel.Creator) if err != nil { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to cancel booking", constants.InternalServerErrorCode, "Failed to cancel booking", nil)) return @@ -138,21 +154,33 @@ func CancelBooking(ctx *gin.Context, appsession *models.AppSession) { // CheckIn handles the check-in process for a booking func CheckIn(ctx *gin.Context, appsession *models.AppSession) { - var checkIn models.CheckIn - - if err := ctx.ShouldBindJSON(&checkIn); err != nil { + var checkInRequest map[string]interface{} + if err := ctx.ShouldBindJSON(&checkInRequest); err != nil { HandleValidationErrors(ctx, err) return } + // Validate JSON + validatedData, err := utils.ValidateJSON(checkInRequest, reflect.TypeOf(models.CheckIn{})) + if err != nil { + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, err.Error(), constants.BadRequestCode, err.Error(), nil)) + return + } + + // Convert validated JSON to CheckIn struct + var checkIn models.CheckIn + checkInBytes, _ := json.Marshal(validatedData) + json.Unmarshal(checkInBytes, &checkIn) + // Check if the booking exists exists := database.BookingExists(ctx, appsession.DB, checkIn.BookingID) if !exists { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to find booking", constants.InternalServerErrorCode, "Failed to find booking", nil)) return } + // Confirm the check-in to the database - _, err := database.ConfirmCheckIn(ctx, appsession.DB, checkIn) + _, err = database.ConfirmCheckIn(ctx, appsession.DB, checkIn) if err != nil { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to check in", constants.InternalServerErrorCode, "Failed to check in. Email not associated with booking", nil)) return @@ -161,7 +189,6 @@ func CheckIn(ctx *gin.Context, appsession *models.AppSession) { ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully checked in!", nil)) } -// View all available rooms func ViewRooms(ctx *gin.Context, appsession *models.AppSession) { var roomRequest map[string]interface{} var room models.RoomRequest @@ -171,13 +198,14 @@ func ViewRooms(ctx *gin.Context, appsession *models.AppSession) { } // Validate JSON - if err := utils.ValidateJSON(roomRequest, reflect.TypeOf(models.RoomRequest{})); err != nil { + validatedData, err := utils.ValidateJSON(roomRequest, reflect.TypeOf(models.RoomRequest{})) + if err != nil { ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, err.Error(), constants.BadRequestCode, err.Error(), nil)) return } // Convert validated JSON to RoomRequest struct - roomBytes, _ := json.Marshal(roomRequest) + roomBytes, _ := json.Marshal(validatedData) json.Unmarshal(roomBytes, &room) var floorNo string @@ -187,7 +215,6 @@ func ViewRooms(ctx *gin.Context, appsession *models.AppSession) { floorNo = room.FloorNo } - var err error var rooms []models.Room rooms, err = database.GetAllRooms(ctx, appsession.DB, floorNo) if err != nil { diff --git a/occupi-backend/tests/authenticator_test.go b/occupi-backend/tests/authenticator_test.go index 1e2b1e4e..10a9cb80 100644 --- a/occupi-backend/tests/authenticator_test.go +++ b/occupi-backend/tests/authenticator_test.go @@ -4,7 +4,6 @@ import ( "testing" "time" - "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator" "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants" "github.com/stretchr/testify/assert" diff --git a/occupi-backend/tests/handlers_test.go b/occupi-backend/tests/handlers_test.go index d1827cd3..05bb9e2d 100644 --- a/occupi-backend/tests/handlers_test.go +++ b/occupi-backend/tests/handlers_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "net/http" "net/http/httptest" - "strings" "sync" "testing" "time" @@ -22,210 +21,224 @@ import ( // "github.com/stretchr/testify/mock" ) -func TestViewBookingsHandler(t *testing.T) { - // connect to the database - db := database.ConnectToDatabase(constants.AdminDBAccessOption) - - // set gin run mode - gin.SetMode(configs.GetGinRunMode()) - - // Create a Gin router - r := gin.Default() - - // Register the route - router.OccupiRouter(r, db) - - token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/ping-auth", nil) - req.AddCookie(&http.Cookie{Name: "token", Value: token}) - - r.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal( - t, - "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are auth'd\",\"status\":200}", - strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"), - ) - - // Store the cookies from the login response - cookies := req.Cookies() - - // Define test cases - testCases := []struct { - name string - email string - expectedStatusCode float64 - expectedMessage string - expectedBookings int - }{ - { - name: "Valid Request", - email: "test.example@gmail.com", - expectedStatusCode: float64(http.StatusOK), - expectedMessage: "Successfully fetched bookings!", - expectedBookings: 2, - }, - { - name: "Invalid Request", - email: "", - expectedStatusCode: float64(http.StatusBadRequest), - expectedMessage: "Invalid request payload", - expectedBookings: 0, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Create a request to pass to the handler - req, err := http.NewRequest("GET", "/api/view-bookings?email="+tc.email, nil) - if err != nil { - t.Fatal(err) - } - - // Add the stored cookies to the request - for _, cookie := range cookies { - req.AddCookie(cookie) - } - - // Create a response recorder to record the response - rr := httptest.NewRecorder() - - // Serve the request - r.ServeHTTP(rr, req) - - // Check the status code is what we expect - assert.Equal(t, tc.expectedStatusCode, float64(rr.Code), "handler returned wrong status code") - - // Define the expected response - expectedResponse := gin.H{ - "message": tc.expectedMessage, - "data": make([]map[string]interface{}, tc.expectedBookings), // Adjust expected data length - "status": tc.expectedStatusCode, - } - - // Unmarshal the actual response - var actualResponse gin.H - if err := json.Unmarshal(rr.Body.Bytes(), &actualResponse); err != nil { - t.Fatalf("could not unmarshal response: %v", err) - } - - // Compare the responses - assert.Equal(t, expectedResponse["message"], actualResponse["message"], "handler returned unexpected message") - assert.Equal(t, expectedResponse["status"], actualResponse["status"], "handler returned unexpected status") - }) - } -} - -/* -func TestBookRoom(t *testing.T) { - // connect to the database - db := database.ConnectToDatabase(constants.AdminDBAccessOption) - - // set gin run mode - gin.SetMode(configs.GetGinRunMode()) - - // Create a Gin router - r := gin.Default() - - // Register the route - router.OccupiRouter(r, db) - - // Generate a token - token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) - - // Ping-auth test to ensure everything is set up correctly - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/ping-auth", nil) - req.AddCookie(&http.Cookie{Name: "token", Value: token}) - - r.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal( - t, - "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are auth'd\",\"status\":200}", - strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"), - ) - - // Store the cookies from the login response - cookies := req.Cookies() - - // Define test cases - testCases := []struct { - name string - payload string - expectedStatusCode int - expectedMessage string - }{ - { - name: "Valid Request", - payload: `{ - "roomID": "12345", - "Slot": 1, - "Emails": ["test@example.com"], - "Creator": "test@example.com", - "FloorNo": 1, - "roomName": "Test Room" - }`, - expectedStatusCode: http.StatusOK, - expectedMessage: "Successfully booked!", - }, - { - name: "Invalid Request Payload", - payload: `{ - "roomID": "", - "Slot": "", - "Emails": [], - "Creator": "", - "FloorNo": 0 - }`, - expectedStatusCode: http.StatusBadRequest, - expectedMessage: "Invalid request payload", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Create a request to pass to the handler - req, err := http.NewRequest("POST", "/api/book-room", bytes.NewBuffer([]byte(tc.payload))) - if err != nil { - t.Fatal(err) - } - - // Add the stored cookies to the request - for _, cookie := range cookies { - req.AddCookie(cookie) - } - - // Create a response recorder to record the response - rr := httptest.NewRecorder() - - // Serve the request - r.ServeHTTP(rr, req) - - // Check the status code is what we expect - assert.Equal(t, tc.expectedStatusCode, rr.Code, "handler returned wrong status code") - - // Check the response message - var actualResponse map[string]interface{} - err = json.Unmarshal(rr.Body.Bytes(), &actualResponse) - if err != nil { - t.Fatalf("could not unmarshal response: %v", err) - } - - assert.Equal(t, tc.expectedMessage, actualResponse["message"], "handler returned unexpected message") - - // For successful booking, check if the ID is generated - if tc.expectedStatusCode == http.StatusOK { - assert.NotEmpty(t, actualResponse["data"], "booking ID should not be empty") - } - }) - } -} -*/ +// func TestViewBookingsHandler(t *testing.T) { +// // Load environment variables from .env file +// if err := godotenv.Load("../.env"); err != nil { +// t.Fatal("Error loading .env file: ", err) +// } + +// // setup logger to log all server interactions +// utils.SetupLogger() + +// // connect to the database +// db := database.ConnectToDatabase() + +// // set gin run mode +// gin.SetMode(configs.GetGinRunMode()) + +// // Create a Gin router +// r := gin.Default() + +// // Register the route +// router.OccupiRouter(r, db) + +// token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) + +// w := httptest.NewRecorder() +// req, _ := http.NewRequest("GET", "/ping-auth", nil) +// req.AddCookie(&http.Cookie{Name: "token", Value: token}) + +// r.ServeHTTP(w, req) + +// assert.Equal(t, http.StatusOK, w.Code) +// assert.Equal( +// t, +// "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are auth'd\",\"status\":200}", +// strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"), +// ) + +// // Store the cookies from the login response +// cookies := req.Cookies() + +// // Define test cases +// testCases := []struct { +// name string +// email string +// expectedStatusCode float64 +// expectedMessage string +// expectedBookings int +// }{ +// { +// name: "Valid Request", +// email: "test.example@gmail.com", +// expectedStatusCode: float64(http.StatusOK), +// expectedMessage: "Successfully fetched bookings!", +// expectedBookings: 2, +// }, +// { +// name: "Invalid Request", +// email: "", +// expectedStatusCode: float64(http.StatusBadRequest), +// expectedMessage: "Invalid request payload", +// expectedBookings: 0, +// }, +// } + +// for _, tc := range testCases { +// t.Run(tc.name, func(t *testing.T) { +// // Create a request to pass to the handler +// req, err := http.NewRequest("GET", "/api/view-bookings?email="+tc.email, nil) +// if err != nil { +// t.Fatal(err) +// } + +// // Add the stored cookies to the request +// for _, cookie := range cookies { +// req.AddCookie(cookie) +// } + +// // Create a response recorder to record the response +// rr := httptest.NewRecorder() + +// // Serve the request +// r.ServeHTTP(rr, req) + +// // Check the status code is what we expect +// assert.Equal(t, tc.expectedStatusCode, float64(rr.Code), "handler returned wrong status code") + +// // Define the expected response +// expectedResponse := gin.H{ +// "message": tc.expectedMessage, +// "data": make([]map[string]interface{}, tc.expectedBookings), // Adjust expected data length +// "status": tc.expectedStatusCode, +// } + +// // Unmarshal the actual response +// var actualResponse gin.H +// if err := json.Unmarshal(rr.Body.Bytes(), &actualResponse); err != nil { +// t.Fatalf("could not unmarshal response: %v", err) +// } + +// // Compare the responses +// assert.Equal(t, expectedResponse["message"], actualResponse["message"], "handler returned unexpected message") +// assert.Equal(t, expectedResponse["status"], actualResponse["status"], "handler returned unexpected status") +// }) +// } +// } + +// func TestBookRoom(t *testing.T) { +// // Load environment variables from .env file +// if err := godotenv.Load("../.env"); err != nil { +// t.Fatal("Error loading .env file: ", err) +// } + +// // Setup logger to log all server interactions +// utils.SetupLogger() + +// // Connect to the database +// db := database.ConnectToDatabase() + +// Set Gin run mode +// gin.SetMode(gin.TestMode) + +// // Create a Gin router +// r := gin.Default() + +// // Register the route +// router.OccupiRouter(r, db) + +// // Generate a token +// token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) + +// // Ping-auth test to ensure everything is set up correctly +// w := httptest.NewRecorder() +// req, _ := http.NewRequest("GET", "/ping-auth", nil) +// req.AddCookie(&http.Cookie{Name: "token", Value: token}) + +// r.ServeHTTP(w, req) + +// assert.Equal(t, http.StatusOK, w.Code) +// assert.Equal( +// t, +// "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are auth'd\",\"status\":200}", +// strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"), +// ) + +// // Store the cookies from the login response +// cookies := req.Cookies() + +// // Define test cases +// testCases := []struct { +// name string +// payload string +// expectedStatusCode int +// expectedMessage string +// }{ +// { +// name: "Valid Request", +// payload: `{ +// "roomID": "12345", +// "Slot": 1, +// "Emails": ["test@example.com"], +// "Creator": "test@example.com", +// "FloorNo": 1, +// "roomName": "Test Room" +// }`, +// expectedStatusCode: http.StatusOK, +// expectedMessage: "Successfully booked!", +// }, +// { +// name: "Invalid Request Payload", +// payload: `{ +// "roomID": "", +// "Slot": "", +// "Emails": [], +// "Creator": "", +// "FloorNo": 0 +// }`, +// expectedStatusCode: http.StatusBadRequest, +// expectedMessage: "Invalid request payload", +// }, +// } + +// for _, tc := range testCases { +// t.Run(tc.name, func(t *testing.T) { +// // Create a request to pass to the handler +// req, err := http.NewRequest("POST", "/api/book-room", bytes.NewBuffer([]byte(tc.payload))) +// if err != nil { +// t.Fatal(err) +// } + +// // Add the stored cookies to the request +// for _, cookie := range cookies { +// req.AddCookie(cookie) +// } + +// // Create a response recorder to record the response +// rr := httptest.NewRecorder() + +// // Serve the request +// r.ServeHTTP(rr, req) + +// // Check the status code is what we expect +// assert.Equal(t, tc.expectedStatusCode, rr.Code, "handler returned wrong status code") + +// // Check the response message +// var actualResponse map[string]interface{} +// err = json.Unmarshal(rr.Body.Bytes(), &actualResponse) +// if err != nil { +// t.Fatalf("could not unmarshal response: %v", err) +// } + +// assert.Equal(t, tc.expectedMessage, actualResponse["message"], "handler returned unexpected message") + +// // For successful booking, check if the ID is generated +// if tc.expectedStatusCode == http.StatusOK { +// assert.NotEmpty(t, actualResponse["data"], "booking ID should not be empty") +// } +// }) +// } +// } func TestPingRoute(t *testing.T) { // connect to the database From 9480f7560e06ca512eb1de7d10f89360feb7da2a Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Fri, 28 Jun 2024 04:04:55 +0200 Subject: [PATCH 43/54] Updated api-handlers --- occupi-backend/pkg/handlers/api_handlers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index 419139cd..380ebf5a 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -104,7 +104,7 @@ func ViewBookings(ctx *gin.Context, appsession *models.AppSession) { // Get all bookings for the userBooking bookings, err := database.GetUserBookings(ctx, appsession.DB, email) if err != nil { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to get bookings", constants.InternalServerErrorCode, "Failed to get bookings", nil)) + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusNotFound, "Failed to get bookings", constants.InternalServerErrorCode, "Failed to get bookings", nil)) return } @@ -175,7 +175,7 @@ func CheckIn(ctx *gin.Context, appsession *models.AppSession) { // Check if the booking exists exists := database.BookingExists(ctx, appsession.DB, checkIn.BookingID) if !exists { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to find booking", constants.InternalServerErrorCode, "Failed to find booking", nil)) + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(404, "Failed to find booking", constants.InternalServerErrorCode, "Failed to find booking", nil)) return } From 7b1fdbe187860c1421e98c664760d7010bda77db Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Fri, 28 Jun 2024 04:23:00 +0200 Subject: [PATCH 44/54] Type checked unmarshal --- occupi-backend/pkg/handlers/api_handlers.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index 380ebf5a..f210999c 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -70,7 +70,10 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { // Convert validated data to Booking struct var booking models.Booking bookingBytes, _ := json.Marshal(validatedData) - json.Unmarshal(bookingBytes, &booking) + if err := json.Unmarshal(bookingBytes, &booking); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to book", constants.InternalServerErrorCode, "Failed to check in", nil)) + return + } // Generate a unique ID for the booking booking.ID = primitive.NewObjectID().Hex() @@ -128,7 +131,10 @@ func CancelBooking(ctx *gin.Context, appsession *models.AppSession) { // Convert validated JSON to Cancel struct var cancel models.Cancel cancelBytes, _ := json.Marshal(validatedData) - json.Unmarshal(cancelBytes, &cancel) + if err := json.Unmarshal(cancelBytes, &cancel); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to cancel", constants.InternalServerErrorCode, "Failed to check in", nil)) + return + } // Check if the booking exists exists := database.BookingExists(ctx, appsession.DB, cancel.ID) @@ -170,7 +176,10 @@ func CheckIn(ctx *gin.Context, appsession *models.AppSession) { // Convert validated JSON to CheckIn struct var checkIn models.CheckIn checkInBytes, _ := json.Marshal(validatedData) - json.Unmarshal(checkInBytes, &checkIn) + if err := json.Unmarshal(checkInBytes, &checkIn); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to check in", constants.InternalServerErrorCode, "Failed to check in", nil)) + return + } // Check if the booking exists exists := database.BookingExists(ctx, appsession.DB, checkIn.BookingID) @@ -206,7 +215,10 @@ func ViewRooms(ctx *gin.Context, appsession *models.AppSession) { // Convert validated JSON to RoomRequest struct roomBytes, _ := json.Marshal(validatedData) - json.Unmarshal(roomBytes, &room) + if err := json.Unmarshal(roomBytes, &room); err != nil { + ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to get room", constants.InternalServerErrorCode, "Failed to check in", nil)) + return + } var floorNo string if room.FloorNo == "" { From ed5ccf3a82023bbf125db18fd793c11363607ebc Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Fri, 28 Jun 2024 19:35:06 +0200 Subject: [PATCH 45/54] "Need to integrate tests" --- occupi-backend/tests/utils_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/occupi-backend/tests/utils_test.go b/occupi-backend/tests/utils_test.go index 44fc2d8a..6c089a9d 100644 --- a/occupi-backend/tests/utils_test.go +++ b/occupi-backend/tests/utils_test.go @@ -457,6 +457,17 @@ func TestValidateJSON(t *testing.T) { expectError: true, errorMessage: "missing required field: field1", }, + { + name: "Invalid type for field", + data: map[string]interface{}{ + "field1": "value1", + "field2": "not-an-int", + "field3": "2024-07-01T09:00:00Z", + }, + expectedType: reflect.TypeOf(SampleStruct{}), + expectError: true, + errorMessage: "field field2 is of incorrect type", + }, { name: "Invalid time format", data: map[string]interface{}{ From ae08b4c401e3f6a032db0cae2aa30cb10f0061d2 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Fri, 28 Jun 2024 21:06:10 +0200 Subject: [PATCH 46/54] All test pass now just need to rewrite integration test --- occupi-backend/go.mod | 12 +++--------- occupi-backend/go.sum | 21 +++------------------ occupi-backend/tests/utils_test.go | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/occupi-backend/go.mod b/occupi-backend/go.mod index 29140271..e6574925 100644 --- a/occupi-backend/go.mod +++ b/occupi-backend/go.mod @@ -6,16 +6,16 @@ toolchain go1.21.6 require ( github.com/alexedwards/argon2id v1.0.0 - github.com/coreos/go-oidc/v3 v3.10.0 + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/gin-contrib/sessions v1.0.1 github.com/gin-gonic/gin v1.10.0 - github.com/joho/godotenv v1.5.1 + github.com/go-playground/validator/v10 v10.20.0 github.com/microcosm-cc/bluemonday v1.0.26 github.com/sirupsen/logrus v1.9.3 + github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 github.com/ulule/limiter/v3 v3.11.2 go.mongodb.org/mongo-driver v1.15.0 - golang.org/x/oauth2 v0.20.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df ) @@ -26,14 +26,11 @@ require ( github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.4 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-jose/go-jose/v4 v4.0.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -45,7 +42,6 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.7 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/kr/pretty v0.3.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -56,14 +52,12 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.19.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect diff --git a/occupi-backend/go.sum b/occupi-backend/go.sum index 6f30fc67..760c7978 100644 --- a/occupi-backend/go.sum +++ b/occupi-backend/go.sum @@ -10,16 +10,14 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= -github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= @@ -30,8 +28,6 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= -github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -59,8 +55,6 @@ github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTj github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= @@ -69,10 +63,8 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -97,14 +89,11 @@ github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8 github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= @@ -179,8 +168,6 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -223,10 +210,8 @@ google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= diff --git a/occupi-backend/tests/utils_test.go b/occupi-backend/tests/utils_test.go index 6c089a9d..68ba1973 100644 --- a/occupi-backend/tests/utils_test.go +++ b/occupi-backend/tests/utils_test.go @@ -501,17 +501,35 @@ func TestTypeCheck(t *testing.T) { expectedType reflect.Type expected bool }{ + // Basic types {"Match int", 42, reflect.TypeOf(42), true}, {"Match string", "hello", reflect.TypeOf("hello"), true}, {"Match float", 3.14, reflect.TypeOf(3.14), true}, {"Mismatch int", "42", reflect.TypeOf(42), false}, {"Mismatch string", 42, reflect.TypeOf("hello"), false}, + + // Pointer types {"Pointer match", new(int), reflect.TypeOf(new(int)), true}, {"Pointer mismatch", new(string), reflect.TypeOf(new(int)), false}, {"Nil pointer", nil, reflect.TypeOf((*int)(nil)), true}, {"Non-nil pointer match", new(int), reflect.TypeOf((*int)(nil)), true}, {"Nil non-pointer", nil, reflect.TypeOf(42), false}, + + // Time type {"Time type valid RFC3339", "2024-07-01T09:00:00Z", reflect.TypeOf(time.Time{}), true}, + {"Time type invalid RFC3339", "not-a-date", reflect.TypeOf(time.Time{}), false}, + + // Slices and arrays + {"Match slice int", []int{1, 2, 3}, reflect.TypeOf([]int{}), true}, + {"Match array int", [3]int{1, 2, 3}, reflect.TypeOf([3]int{}), true}, + {"Mismatch slice int", []string{"1", "2", "3"}, reflect.TypeOf([]int{}), false}, + {"Mismatch array int", [3]string{"1", "2", "3"}, reflect.TypeOf([3]int{}), false}, + {"Empty slice", []int{}, reflect.TypeOf([]int{}), true}, + {"Empty array", [0]int{}, reflect.TypeOf([0]int{}), true}, + + // Nested slices/arrays + {"Match nested slice int", [][]int{{1, 2}, {3, 4}}, reflect.TypeOf([][]int{}), true}, + {"Mismatch nested slice int", [][]string{{"1", "2"}, {"3", "4"}}, reflect.TypeOf([][]int{}), false}, } for _, tt := range tests { From 7dac7fd5fb62e5d9187594f3e9addd10baae2fd1 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Fri, 28 Jun 2024 23:36:16 +0200 Subject: [PATCH 47/54] Working on integration tests --- occupi-backend/pkg/handlers/api_handlers.go | 4 +- occupi-backend/pkg/mail/mail.go | 4 +- occupi-backend/pkg/models/database.go | 18 +- occupi-backend/pkg/utils/utils.go | 13 +- occupi-backend/tests/handlers_test.go | 584 ++++++++++++-------- 5 files changed, 386 insertions(+), 237 deletions(-) diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index f210999c..e4e958ce 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -63,7 +63,7 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) { // Validate JSON validatedData, err := utils.ValidateJSON(bookingRequest, reflect.TypeOf(models.Booking{})) if err != nil { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, err.Error(), constants.BadRequestCode, err.Error(), nil)) + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.BadRequestCode, err.Error(), nil)) return } @@ -124,7 +124,7 @@ func CancelBooking(ctx *gin.Context, appsession *models.AppSession) { // Validate JSON validatedData, err := utils.ValidateJSON(cancelRequest, reflect.TypeOf(models.Cancel{})) if err != nil { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, err.Error(), constants.BadRequestCode, err.Error(), nil)) + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.BadRequestCode, err.Error(), nil)) return } diff --git a/occupi-backend/pkg/mail/mail.go b/occupi-backend/pkg/mail/mail.go index 80421864..1e7d8c09 100644 --- a/occupi-backend/pkg/mail/mail.go +++ b/occupi-backend/pkg/mail/mail.go @@ -89,11 +89,11 @@ func SendBookingEmails(booking models.Booking) error { func SendCancellationEmails(cancel models.Cancel) error { // Prepare the email content creatorSubject := "Booking Cancelled - Occupi" - creatorBody := FormatCancellationEmailBodyForBooker(cancel.ID, cancel.RoomID, 0, cancel.Creator) + creatorBody := FormatCancellationEmailBodyForBooker(cancel.BookingID, cancel.RoomID, 0, cancel.Creator) // Prepare the email content for attendees attendeesSubject := "Booking Cancelled - Occupi" - attendeesBody := FormatCancellationEmailBodyForAttendees(cancel.ID, cancel.RoomID, 0, cancel.Creator) + attendeesBody := FormatCancellationEmailBodyForAttendees(cancel.BookingID, cancel.RoomID, 0, cancel.Creator) var attendees []string for _, email := range cancel.Emails { diff --git a/occupi-backend/pkg/models/database.go b/occupi-backend/pkg/models/database.go index 8e2a2f64..1f5220e3 100644 --- a/occupi-backend/pkg/models/database.go +++ b/occupi-backend/pkg/models/database.go @@ -29,15 +29,15 @@ type Booking struct { End time.Time `json:"end" bson:"end" binding:"required"` } type Cancel struct { - ID string `json:"_id" bson:"_id,omitempty"` - RoomID string `json:"roomId" bson:"roomId" binding:"required"` - RoomName string `json:"roomName" bson:"roomName" binding:"required"` - Emails []string `json:"emails" bson:"emails" binding:"required,dive,email"` - Creator string `json:"creator" bson:"creator" binding:"required,email"` - FloorNo string `json:"floorNo" bson:"floorNo" binding:"required"` - Date time.Time `json:"date" bson:"date" binding:"required"` - Start time.Time `json:"start" bson:"start" binding:"required"` - End time.Time `json:"end" bson:"end" binding:"required"` + BookingID string `json:"bookingId" bson:"bookingId" binding:"required"` + RoomID string `json:"roomId" bson:"roomId" binding:"required"` + RoomName string `json:"roomName" bson:"roomName" binding:"required"` + Emails []string `json:"emails" bson:"emails" binding:"required,dive,email"` + Creator string `json:"creator" bson:"creator" binding:"required,email"` + FloorNo string `json:"floorNo" bson:"floorNo" binding:"required"` + Date time.Time `json:"date" bson:"date" binding:"required"` + Start time.Time `json:"start" bson:"start" binding:"required"` + End time.Time `json:"end" bson:"end" binding:"required"` } // structure of CheckIn diff --git a/occupi-backend/pkg/utils/utils.go b/occupi-backend/pkg/utils/utils.go index c3452bb1..dbbbeadf 100644 --- a/occupi-backend/pkg/utils/utils.go +++ b/occupi-backend/pkg/utils/utils.go @@ -235,12 +235,6 @@ func ValidateJSON(data map[string]interface{}, expectedType reflect.Type) (map[s continue } - // Check the field type - if !TypeCheck(value, field.Type) { - logrus.Error("field ", jsonTag, " is of incorrect type") - return nil, fmt.Errorf("field %s is of incorrect type", jsonTag) - } - // Parse date/time strings to time.Time if field.Type == reflect.TypeOf(time.Time{}) { parsedTime, err := time.Parse(time.RFC3339, value.(string)) @@ -252,6 +246,13 @@ func ValidateJSON(data map[string]interface{}, expectedType reflect.Type) (map[s } else { validatedData[jsonTag] = value } + + // Check the field type + if !TypeCheck(value, field.Type) { + logrus.Error("field ", jsonTag, " is of incorrect type") + return nil, fmt.Errorf("field %s is of incorrect type", jsonTag) + } + } return validatedData, nil } diff --git a/occupi-backend/tests/handlers_test.go b/occupi-backend/tests/handlers_test.go index 2946d304..cf20a385 100644 --- a/occupi-backend/tests/handlers_test.go +++ b/occupi-backend/tests/handlers_test.go @@ -1,9 +1,12 @@ package tests import ( + "bytes" "encoding/json" + "fmt" "net/http" "net/http/httptest" + "strings" "sync" "testing" "time" @@ -21,224 +24,369 @@ import ( // "github.com/stretchr/testify/mock" ) -// func TestViewBookingsHandler(t *testing.T) { -// // Load environment variables from .env file -// if err := godotenv.Load("../.env"); err != nil { -// t.Fatal("Error loading .env file: ", err) -// } - -// // setup logger to log all server interactions -// utils.SetupLogger() - -// // connect to the database -// db := database.ConnectToDatabase() - -// set gin run mode -// gin.SetMode("test") - -// // Create a Gin router -// r := gin.Default() - -// // Register the route -// router.OccupiRouter(r, db) - -// token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) - -// w := httptest.NewRecorder() -// req, _ := http.NewRequest("GET", "/ping-auth", nil) -// req.AddCookie(&http.Cookie{Name: "token", Value: token}) - -// r.ServeHTTP(w, req) - -// assert.Equal(t, http.StatusOK, w.Code) -// assert.Equal( -// t, -// "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are auth'd\",\"status\":200}", -// strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"), -// ) - -// // Store the cookies from the login response -// cookies := req.Cookies() - -// // Define test cases -// testCases := []struct { -// name string -// email string -// expectedStatusCode float64 -// expectedMessage string -// expectedBookings int -// }{ -// { -// name: "Valid Request", -// email: "test.example@gmail.com", -// expectedStatusCode: float64(http.StatusOK), -// expectedMessage: "Successfully fetched bookings!", -// expectedBookings: 2, -// }, -// { -// name: "Invalid Request", -// email: "", -// expectedStatusCode: float64(http.StatusBadRequest), -// expectedMessage: "Invalid request payload", -// expectedBookings: 0, -// }, -// } - -// for _, tc := range testCases { -// t.Run(tc.name, func(t *testing.T) { -// // Create a request to pass to the handler -// req, err := http.NewRequest("GET", "/api/view-bookings?email="+tc.email, nil) -// if err != nil { -// t.Fatal(err) -// } - -// // Add the stored cookies to the request -// for _, cookie := range cookies { -// req.AddCookie(cookie) -// } - -// // Create a response recorder to record the response -// rr := httptest.NewRecorder() - -// // Serve the request -// r.ServeHTTP(rr, req) - -// // Check the status code is what we expect -// assert.Equal(t, tc.expectedStatusCode, float64(rr.Code), "handler returned wrong status code") - -// // Define the expected response -// expectedResponse := gin.H{ -// "message": tc.expectedMessage, -// "data": make([]map[string]interface{}, tc.expectedBookings), // Adjust expected data length -// "status": tc.expectedStatusCode, -// } - -// // Unmarshal the actual response -// var actualResponse gin.H -// if err := json.Unmarshal(rr.Body.Bytes(), &actualResponse); err != nil { -// t.Fatalf("could not unmarshal response: %v", err) -// } - -// // Compare the responses -// assert.Equal(t, expectedResponse["message"], actualResponse["message"], "handler returned unexpected message") -// assert.Equal(t, expectedResponse["status"], actualResponse["status"], "handler returned unexpected status") -// }) -// } -// } - -// func TestBookRoom(t *testing.T) { -// // Load environment variables from .env file -// if err := godotenv.Load("../.env"); err != nil { -// t.Fatal("Error loading .env file: ", err) -// } - -// // Setup logger to log all server interactions -// utils.SetupLogger() - -// // Connect to the database -// db := database.ConnectToDatabase() - -// set gin run mode -// gin.SetMode("test") - -// // Create a Gin router -// r := gin.Default() - -// // Register the route -// router.OccupiRouter(r, db) - -// // Generate a token -// token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) - -// // Ping-auth test to ensure everything is set up correctly -// w := httptest.NewRecorder() -// req, _ := http.NewRequest("GET", "/ping-auth", nil) -// req.AddCookie(&http.Cookie{Name: "token", Value: token}) - -// r.ServeHTTP(w, req) - -// assert.Equal(t, http.StatusOK, w.Code) -// assert.Equal( -// t, -// "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are auth'd\",\"status\":200}", -// strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"), -// ) - -// // Store the cookies from the login response -// cookies := req.Cookies() - -// // Define test cases -// testCases := []struct { -// name string -// payload string -// expectedStatusCode int -// expectedMessage string -// }{ -// { -// name: "Valid Request", -// payload: `{ -// "roomID": "12345", -// "Slot": 1, -// "Emails": ["test@example.com"], -// "Creator": "test@example.com", -// "FloorNo": 1, -// "roomName": "Test Room" -// }`, -// expectedStatusCode: http.StatusOK, -// expectedMessage: "Successfully booked!", -// }, -// { -// name: "Invalid Request Payload", -// payload: `{ -// "roomID": "", -// "Slot": "", -// "Emails": [], -// "Creator": "", -// "FloorNo": 0 -// }`, -// expectedStatusCode: http.StatusBadRequest, -// expectedMessage: "Invalid request payload", -// }, -// } - -// for _, tc := range testCases { -// t.Run(tc.name, func(t *testing.T) { -// // Create a request to pass to the handler -// req, err := http.NewRequest("POST", "/api/book-room", bytes.NewBuffer([]byte(tc.payload))) -// if err != nil { -// t.Fatal(err) -// } - -// // Add the stored cookies to the request -// for _, cookie := range cookies { -// req.AddCookie(cookie) -// } - -// // Create a response recorder to record the response -// rr := httptest.NewRecorder() - -// // Serve the request -// r.ServeHTTP(rr, req) - -// // Check the status code is what we expect -// assert.Equal(t, tc.expectedStatusCode, rr.Code, "handler returned wrong status code") - -// // Check the response message -// var actualResponse map[string]interface{} -// err = json.Unmarshal(rr.Body.Bytes(), &actualResponse) -// if err != nil { -// t.Fatalf("could not unmarshal response: %v", err) -// } - -// assert.Equal(t, tc.expectedMessage, actualResponse["message"], "handler returned unexpected message") - -// // For successful booking, check if the ID is generated -// if tc.expectedStatusCode == http.StatusOK { -// assert.NotEmpty(t, actualResponse["data"], "booking ID should not be empty") -// } -// }) -// } -// } +func TestViewBookingsHandler(t *testing.T) { + // connect to the database + db := database.ConnectToDatabase(constants.AdminDBAccessOption) + + // set gin run mode + gin.SetMode(configs.GetGinRunMode()) + + // Create a Gin router + r := gin.Default() + + // Register the route + router.OccupiRouter(r, db) + + token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/ping-auth", nil) + req.AddCookie(&http.Cookie{Name: "token", Value: token}) + + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal( + t, + "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are auth'd\",\"status\":200}", + strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"), + ) + + // Store the cookies from the login response + cookies := req.Cookies() + + // Define test cases + testCases := []struct { + name string + email string + expectedStatusCode float64 + expectedMessage string + expectedBookings int + }{ + { + name: "Valid Request", + email: "test.example@gmail.com", + expectedStatusCode: float64(http.StatusOK), + expectedMessage: "Successfully fetched bookings!", + expectedBookings: 2, + }, + { + name: "Invalid Request", + email: "", + expectedStatusCode: float64(http.StatusBadRequest), + expectedMessage: "Invalid request payload", + expectedBookings: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create a request to pass to the handler + req, err := http.NewRequest("GET", "/api/view-bookings?email="+tc.email, nil) + if err != nil { + t.Fatal(err) + } + + // Add the stored cookies to the request + for _, cookie := range cookies { + req.AddCookie(cookie) + } + + // Create a response recorder to record the response + rr := httptest.NewRecorder() + + // Serve the request + r.ServeHTTP(rr, req) + + // Check the status code is what we expect + assert.Equal(t, tc.expectedStatusCode, float64(rr.Code), "handler returned wrong status code") + + // Define the expected response + expectedResponse := gin.H{ + "message": tc.expectedMessage, + "data": make([]map[string]interface{}, tc.expectedBookings), // Adjust expected data length + "status": tc.expectedStatusCode, + } + + // Unmarshal the actual response + var actualResponse gin.H + if err := json.Unmarshal(rr.Body.Bytes(), &actualResponse); err != nil { + t.Fatalf("could not unmarshal response: %v", err) + } + + // Compare the responses + assert.Equal(t, expectedResponse["message"], actualResponse["message"], "handler returned unexpected message") + assert.Equal(t, expectedResponse["status"], actualResponse["status"], "handler returned unexpected status") + }) + } +} + +func createMockBooking(r *gin.Engine, payload string, cookies []*http.Cookie) error { + req, err := http.NewRequest("POST", "/api/book-room", bytes.NewBuffer([]byte(payload))) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + + // Add the stored cookies to the request + for _, cookie := range cookies { + req.AddCookie(cookie) + } + + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + return fmt.Errorf("expected status 200 but got %d", rr.Code) + } + + return nil +} +func TestCancelBooking(t *testing.T) { + // Connect to the test database + db := database.ConnectToDatabase(constants.AdminDBAccessOption) + + // Set Gin run mode + gin.SetMode(configs.GetGinRunMode()) + + // Create a Gin router + r := gin.Default() + + // Register the route + router.OccupiRouter(r, db) + + // Generate a token + token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) + + // Ping-auth test to ensure everything is set up correctly + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/ping-auth", nil) + req.AddCookie(&http.Cookie{Name: "token", Value: token}) + + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal( + t, + "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are auth'd\",\"status\":200}", + strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"), + ) + + // Store the cookies from the login response + cookies := req.Cookies() + + // Define test cases + testCases := []struct { + name string + payload string + expectedStatusCode int + expectedMessage string + setupFunc func() + }{ + { + name: "Valid Request", + payload: `{ + "_id": "5f8f8f8f8f8f8f8f8f8f8f8", + "roomId": "12345", + "emails": ["test@example.com"], + "creator": "test@example.com", + "floorNo": "1", + "roomName": "Test Room", + "date": "2024-07-01T09:00:00Z", + "start": "2024-07-01T09:00:00Z", + "end": "2024-07-01T10:00:00Z" + }`, + expectedStatusCode: http.StatusOK, + expectedMessage: "Successfully cancelled booking!", + setupFunc: func() { + // Insert a booking to be cancelled using the helper function + bookingPayload := `{ + "_id": "5f8f8f8f8f8f8f8f8f8f8f8", + "roomId": "12345", + "emails": ["test@example.com"], + "creator": "test@example.com", + "floorNo": "1", + "roomName": "Test Room", + "date": "2024-07-01T09:00:00Z", + "start": "2024-07-01T09:00:00Z", + "end": "2024-07-01T10:00:00Z" + }` + + err := createMockBooking(r, bookingPayload, cookies) + if err != nil { + t.Fatalf("could not create mock booking: %v", err) + } + }, + }, + { + name: "Invalid Request Payload", + payload: `{ + "id": "", + "creator": "" + }`, + expectedStatusCode: http.StatusBadRequest, + expectedMessage: "Invalid request payload", + setupFunc: func() {}, + }, + { + name: "Booking Not Found", + payload: `{ + "id": "nonexistent", + "creator": "test@example.com", + "roomId": "12345" + }`, + expectedStatusCode: http.StatusNotFound, + expectedMessage: "Booking not found", + setupFunc: func() {}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Setup the test case + tc.setupFunc() + + // Create a request to pass to the handler + req, err := http.NewRequest("POST", "/api/cancel-booking", bytes.NewBuffer([]byte(tc.payload))) + if err != nil { + t.Fatal(err) + } + + // Add the stored cookies to the request + for _, cookie := range cookies { + req.AddCookie(cookie) + } + + // Create a response recorder to record the response + rr := httptest.NewRecorder() + + // Serve the request + r.ServeHTTP(rr, req) + + // Check the status code is what we expect + assert.Equal(t, tc.expectedStatusCode, rr.Code, "handler returned wrong status code") + + // Check the response message + var actualResponse map[string]interface{} + err = json.Unmarshal(rr.Body.Bytes(), &actualResponse) + if err != nil { + t.Fatalf("could not unmarshal response: %v", err) + } + + assert.Equal(t, tc.expectedMessage, actualResponse["message"], "handler returned unexpected message") + }) + } +} +func TestBookRoom(t *testing.T) { + // Connect to the test database + db := database.ConnectToDatabase(constants.AdminDBAccessOption) + + // Set Gin run mode + gin.SetMode(configs.GetGinRunMode()) + + // Create a Gin router + r := gin.Default() + + // Register the route + router.OccupiRouter(r, db) + + // Generate a token + token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) + + // Ping-auth test to ensure everything is set up correctly + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/ping-auth", nil) + req.AddCookie(&http.Cookie{Name: "token", Value: token}) + + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal( + t, + "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are auth'd\",\"status\":200}", + strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"), + ) + + // Store the cookies from the login response + cookies := req.Cookies() + + // Define test cases + testCases := []struct { + name string + payload string + expectedStatusCode int + expectedMessage string + }{ + { + name: "Valid Request", + payload: `{ + "roomId": "12345", + "emails": ["test@example.com"], + "creator": "test@example.com", + "floorNo": "1", + "roomName": "Test Room", + "date": "2024-07-01T00:00:00Z", + "start": "2024-07-01T09:00:00Z", + "end": "2024-07-01T10:00:00Z" + }`, + expectedStatusCode: http.StatusOK, + expectedMessage: "Successfully booked!", + }, + { + name: "Invalid Request Payload", + payload: `{ + "roomId": "", + "emails": [], + "creator": "", + "floorNo": "0", + "roomName": "", + "date": "", + "start": "", + "end": "" + }`, + expectedStatusCode: http.StatusBadRequest, + expectedMessage: "Invalid request payload", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create a request to pass to the handler + req, err := http.NewRequest("POST", "/api/book-room", bytes.NewBuffer([]byte(tc.payload))) + if err != nil { + t.Fatal(err) + } + + // Add the stored cookies to the request + for _, cookie := range cookies { + req.AddCookie(cookie) + } + + // Create a response recorder to record the response + rr := httptest.NewRecorder() + + // Serve the request + r.ServeHTTP(rr, req) + + // Check the status code is what we expect + assert.Equal(t, tc.expectedStatusCode, rr.Code, "handler returned wrong status code") + + // Check the response message + var actualResponse map[string]interface{} + err = json.Unmarshal(rr.Body.Bytes(), &actualResponse) + if err != nil { + t.Fatalf("could not unmarshal response: %v", err) + } + + assert.Equal(t, tc.expectedMessage, actualResponse["message"], "handler returned unexpected message") + + // For successful booking, check if the ID is generated + if tc.expectedStatusCode == http.StatusOK { + assert.NotEmpty(t, actualResponse["data"], "booking ID should not be empty") + } + }) + } +} func TestPingRoute(t *testing.T) { // connect to the database From 9c3c8f63461f243cf24ebe321fd65ff8c855b000 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Sat, 29 Jun 2024 01:39:45 +0200 Subject: [PATCH 48/54] "Done integration testing" --- occupi-backend/pkg/database/database.go | 1 - occupi-backend/pkg/handlers/api_handlers.go | 10 +- occupi-backend/pkg/models/database.go | 1 - occupi-backend/tests/handlers_test.go | 214 +++++++++++++++++--- 4 files changed, 187 insertions(+), 39 deletions(-) diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go index ca3a8f0d..3c4b826e 100644 --- a/occupi-backend/pkg/database/database.go +++ b/occupi-backend/pkg/database/database.go @@ -135,7 +135,6 @@ func ConfirmCheckIn(ctx *gin.Context, db *mongo.Client, checkIn models.CheckIn) // Find the booking by bookingId, roomId, and creator filter := bson.M{ "_id": checkIn.BookingID, - "roomId": checkIn.RoomID, "creator": checkIn.Creator, } diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go index e4e958ce..0903f0a4 100644 --- a/occupi-backend/pkg/handlers/api_handlers.go +++ b/occupi-backend/pkg/handlers/api_handlers.go @@ -137,14 +137,14 @@ func CancelBooking(ctx *gin.Context, appsession *models.AppSession) { } // Check if the booking exists - exists := database.BookingExists(ctx, appsession.DB, cancel.ID) + exists := database.BookingExists(ctx, appsession.DB, cancel.BookingID) if !exists { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusNotFound, "Booking not found", constants.InternalServerErrorCode, "Booking not found", nil)) + ctx.JSON(http.StatusNotFound, utils.ErrorResponse(http.StatusNotFound, "Booking not found", constants.InternalServerErrorCode, "Booking not found", nil)) return } // Confirm the cancellation to the database - _, err = database.ConfirmCancellation(ctx, appsession.DB, cancel.ID, cancel.Creator) + _, err = database.ConfirmCancellation(ctx, appsession.DB, cancel.BookingID, cancel.Creator) if err != nil { ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to cancel booking", constants.InternalServerErrorCode, "Failed to cancel booking", nil)) return @@ -169,7 +169,7 @@ func CheckIn(ctx *gin.Context, appsession *models.AppSession) { // Validate JSON validatedData, err := utils.ValidateJSON(checkInRequest, reflect.TypeOf(models.CheckIn{})) if err != nil { - ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, err.Error(), constants.BadRequestCode, err.Error(), nil)) + ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.BadRequestCode, err.Error(), nil)) return } @@ -184,7 +184,7 @@ func CheckIn(ctx *gin.Context, appsession *models.AppSession) { // Check if the booking exists exists := database.BookingExists(ctx, appsession.DB, checkIn.BookingID) if !exists { - ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(404, "Failed to find booking", constants.InternalServerErrorCode, "Failed to find booking", nil)) + ctx.JSON(http.StatusNotFound, utils.ErrorResponse(http.StatusNotFound, "Booking not found", constants.InternalServerErrorCode, "Booking not found", nil)) return } diff --git a/occupi-backend/pkg/models/database.go b/occupi-backend/pkg/models/database.go index 1f5220e3..1ddeb0b5 100644 --- a/occupi-backend/pkg/models/database.go +++ b/occupi-backend/pkg/models/database.go @@ -44,7 +44,6 @@ type Cancel struct { type CheckIn struct { BookingID string `json:"bookingId" bson:"bookingId" binding:"required"` Creator string `json:"creator" bson:"creator" binding:"required,email"` - RoomID string `json:"roomId" bson:"roomId" binding:"required"` } type OTP struct { diff --git a/occupi-backend/tests/handlers_test.go b/occupi-backend/tests/handlers_test.go index cf20a385..53c03503 100644 --- a/occupi-backend/tests/handlers_test.go +++ b/occupi-backend/tests/handlers_test.go @@ -121,13 +121,13 @@ func TestViewBookingsHandler(t *testing.T) { } } -func createMockBooking(r *gin.Engine, payload string, cookies []*http.Cookie) error { +func createMockBooking(r *gin.Engine, payload string, cookies []*http.Cookie) (map[string]interface{}, error) { req, err := http.NewRequest("POST", "/api/book-room", bytes.NewBuffer([]byte(payload))) if err != nil { - return err + return nil, err } - req.Header.Set("Content-Type", "application/json") + req.Header.Set("Content-Type", "application/json") // Add the stored cookies to the request for _, cookie := range cookies { req.AddCookie(cookie) @@ -137,11 +137,18 @@ func createMockBooking(r *gin.Engine, payload string, cookies []*http.Cookie) er r.ServeHTTP(rr, req) if rr.Code != http.StatusOK { - return fmt.Errorf("expected status 200 but got %d", rr.Code) + return nil, fmt.Errorf("expected status 200 but got %d", rr.Code) } - return nil + var response map[string]interface{} + err = json.Unmarshal(rr.Body.Bytes(), &response) + if err != nil { + return nil, fmt.Errorf("could not unmarshal response: %v", err) + } + + return response, nil } + func TestCancelBooking(t *testing.T) { // Connect to the test database db := database.ConnectToDatabase(constants.AdminDBAccessOption) @@ -181,28 +188,14 @@ func TestCancelBooking(t *testing.T) { payload string expectedStatusCode int expectedMessage string - setupFunc func() + setupFunc func() string // Return booking ID for valid setup }{ { name: "Valid Request", payload: `{ - "_id": "5f8f8f8f8f8f8f8f8f8f8f8", - "roomId": "12345", - "emails": ["test@example.com"], - "creator": "test@example.com", - "floorNo": "1", - "roomName": "Test Room", - "date": "2024-07-01T09:00:00Z", - "start": "2024-07-01T09:00:00Z", - "end": "2024-07-01T10:00:00Z" - }`, - expectedStatusCode: http.StatusOK, - expectedMessage: "Successfully cancelled booking!", - setupFunc: func() { - // Insert a booking to be cancelled using the helper function - bookingPayload := `{ - "_id": "5f8f8f8f8f8f8f8f8f8f8f8", - "roomId": "12345", + "bookingId": "mock_id", + "creator": "test@example.com", + "roomId": "12345", "emails": ["test@example.com"], "creator": "test@example.com", "floorNo": "1", @@ -210,12 +203,26 @@ func TestCancelBooking(t *testing.T) { "date": "2024-07-01T09:00:00Z", "start": "2024-07-01T09:00:00Z", "end": "2024-07-01T10:00:00Z" + }`, + expectedStatusCode: http.StatusOK, + expectedMessage: "Successfully cancelled booking!", + setupFunc: func() string { + // Insert a booking to be cancelled using the helper function + bookingPayload := `{ + "roomId": "12345", + "emails": ["test@example.com"], + "creator": "test@example.com", + "floorNo": "1", + "roomName": "Test Room", + "date": "2024-07-01T00:00:00Z", + "start": "2024-07-01T09:00:00Z", + "end": "2024-07-01T10:00:00Z" }` - - err := createMockBooking(r, bookingPayload, cookies) + response, err := createMockBooking(r, bookingPayload, cookies) if err != nil { t.Fatalf("could not create mock booking: %v", err) } + return response["data"].(string) // Assuming "data" contains the booking ID }, }, { @@ -226,25 +233,37 @@ func TestCancelBooking(t *testing.T) { }`, expectedStatusCode: http.StatusBadRequest, expectedMessage: "Invalid request payload", - setupFunc: func() {}, + setupFunc: func() string { return "" }, }, { name: "Booking Not Found", payload: `{ - "id": "nonexistent", - "creator": "test@example.com", - "roomId": "12345" + "bookingId": "nonexistent", + "creator": "test@example.com", + "roomId": "12345", + "emails": ["test@example.com"], + "creator": "test@example.com", + "floorNo": "1", + "roomName": "Test Room", + "date": "2024-07-01T09:00:00Z", + "start": "2024-07-01T09:00:00Z", + "end": "2024-07-01T10:00:00Z" }`, expectedStatusCode: http.StatusNotFound, expectedMessage: "Booking not found", - setupFunc: func() {}, + setupFunc: func() string { return "" }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - // Setup the test case - tc.setupFunc() + // Setup the test case and get the booking ID if applicable + bookingID := tc.setupFunc() + + // Replace the mock_id placeholder in the payload with the actual booking ID + if bookingID != "" { + tc.payload = strings.Replace(tc.payload, "mock_id", bookingID, 1) + } // Create a request to pass to the handler req, err := http.NewRequest("POST", "/api/cancel-booking", bytes.NewBuffer([]byte(tc.payload))) @@ -277,6 +296,7 @@ func TestCancelBooking(t *testing.T) { }) } } + func TestBookRoom(t *testing.T) { // Connect to the test database db := database.ConnectToDatabase(constants.AdminDBAccessOption) @@ -387,7 +407,137 @@ func TestBookRoom(t *testing.T) { }) } } +func TestCheckIn(t *testing.T) { + // Connect to the test database + db := database.ConnectToDatabase(constants.AdminDBAccessOption) + + // Set Gin run mode + gin.SetMode(configs.GetGinRunMode()) + + // Create a Gin router + r := gin.Default() + + // Register the route + router.OccupiRouter(r, db) + + // Generate a token + token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) + // Ping-auth test to ensure everything is set up correctly + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/ping-auth", nil) + req.AddCookie(&http.Cookie{Name: "token", Value: token}) + + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal( + t, + "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are auth'd\",\"status\":200}", + strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"), + ) + + // Store the cookies from the login response + cookies := req.Cookies() + + // Define test cases + testCases := []struct { + name string + payload string + expectedStatusCode int + expectedMessage string + setupFunc func() string // Return booking ID for valid setup + }{ + { + name: "Valid Request", + payload: `{ + "bookingId": "mock_id", + "creator": "test@example.com" + }`, + expectedStatusCode: http.StatusOK, + expectedMessage: "Successfully checked in!", + setupFunc: func() string { + // Insert a booking to be cancelled using the helper function + bookingPayload := `{ + "roomId": "12345", + "emails": ["test@example.com"], + "creator": "test@example.com", + "floorNo": "1", + "roomName": "Test Room", + "date": "2024-07-01T00:00:00Z", + "start": "2024-07-01T09:00:00Z", + "end": "2024-07-01T10:00:00Z" + }` + response, err := createMockBooking(r, bookingPayload, cookies) + if err != nil { + t.Fatalf("could not create mock booking: %v", err) + } + return response["data"].(string) // Assuming "data" contains the booking ID + }, + }, + { + name: "Invalid Request Payload", + payload: `{ + "bookingID": "", + "creator": "test@example.com" + }`, + expectedStatusCode: http.StatusBadRequest, + expectedMessage: "Invalid request payload", + setupFunc: func() string { return "" }, + }, + { + name: "Booking Not Found", + payload: `{ + "bookingId": "nonexistent", + "creator": "test@example.com" + }`, + expectedStatusCode: http.StatusNotFound, + expectedMessage: "Booking not found", + setupFunc: func() string { return "" }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Setup the test case and get the booking ID if applicable + bookingID := tc.setupFunc() + + // Replace the mock_id placeholder in the payload with the actual booking ID + if bookingID != "" { + tc.payload = strings.Replace(tc.payload, "mock_id", bookingID, 1) + } + + // Create a request to pass to the handler + req, err := http.NewRequest("POST", "/api/check-in", bytes.NewBuffer([]byte(tc.payload))) + if err != nil { + t.Fatal(err) + } + + // Add the stored cookies to the request + for _, cookie := range cookies { + req.AddCookie(cookie) + } + + // Create a response recorder to record the response + rr := httptest.NewRecorder() + + // Serve the request + r.ServeHTTP(rr, req) + + // Check the status code is what we expect + assert.Equal(t, tc.expectedStatusCode, rr.Code, "handler returned wrong status code") + + // Check the response message + var actualResponse map[string]interface{} + err = json.Unmarshal(rr.Body.Bytes(), &actualResponse) + if err != nil { + t.Fatalf("could not unmarshal response: %v", err) + } + + assert.Equal(t, tc.expectedMessage, actualResponse["message"], "handler returned unexpected message") + }) + } +} func TestPingRoute(t *testing.T) { // connect to the database db := database.ConnectToDatabase(constants.AdminDBAccessOption) From 3b51dfb3741bcea79a551da4a9e74bc9fb0b5af3 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Sat, 29 Jun 2024 01:49:00 +0200 Subject: [PATCH 49/54] "Fixed my errors" --- occupi-backend/tests/handlers_test.go | 8 ++++---- occupi-backend/tests/utils_test.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/occupi-backend/tests/handlers_test.go b/occupi-backend/tests/handlers_test.go index 53c03503..110194a9 100644 --- a/occupi-backend/tests/handlers_test.go +++ b/occupi-backend/tests/handlers_test.go @@ -460,8 +460,8 @@ func TestCheckIn(t *testing.T) { // Insert a booking to be cancelled using the helper function bookingPayload := `{ "roomId": "12345", - "emails": ["test@example.com"], - "creator": "test@example.com", + "emails": ["test2@example.com"], + "creator": "test2@example.com", "floorNo": "1", "roomName": "Test Room", "date": "2024-07-01T00:00:00Z", @@ -479,7 +479,7 @@ func TestCheckIn(t *testing.T) { name: "Invalid Request Payload", payload: `{ "bookingID": "", - "creator": "test@example.com" + "creator": "test2@example.com" }`, expectedStatusCode: http.StatusBadRequest, expectedMessage: "Invalid request payload", @@ -489,7 +489,7 @@ func TestCheckIn(t *testing.T) { name: "Booking Not Found", payload: `{ "bookingId": "nonexistent", - "creator": "test@example.com" + "creator": "test2@example.com" }`, expectedStatusCode: http.StatusNotFound, expectedMessage: "Booking not found", diff --git a/occupi-backend/tests/utils_test.go b/occupi-backend/tests/utils_test.go index 68ba1973..38380de0 100644 --- a/occupi-backend/tests/utils_test.go +++ b/occupi-backend/tests/utils_test.go @@ -477,7 +477,7 @@ func TestValidateJSON(t *testing.T) { }, expectedType: reflect.TypeOf(SampleStruct{}), expectError: true, - errorMessage: "field field3 is of incorrect type", + errorMessage: "field field3 is of incorrect format", }, } From 1aeeb9315b43f0bb15106bb8802a69b7e746c4a6 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Sat, 29 Jun 2024 01:54:19 +0200 Subject: [PATCH 50/54] "Need to fix linting. Lines aren't identical" --- occupi-backend/tests/handlers_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/occupi-backend/tests/handlers_test.go b/occupi-backend/tests/handlers_test.go index 110194a9..ffd3ec17 100644 --- a/occupi-backend/tests/handlers_test.go +++ b/occupi-backend/tests/handlers_test.go @@ -452,7 +452,7 @@ func TestCheckIn(t *testing.T) { name: "Valid Request", payload: `{ "bookingId": "mock_id", - "creator": "test@example.com" + "creator": "test2@example.com" }`, expectedStatusCode: http.StatusOK, expectedMessage: "Successfully checked in!", From 568e4a2ea47b77e1138b575bcb543ea406592558 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Sat, 29 Jun 2024 23:41:26 +0200 Subject: [PATCH 51/54] "Created a setup test function" --- occupi-backend/tests/handlers_test.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/occupi-backend/tests/handlers_test.go b/occupi-backend/tests/handlers_test.go index ffd3ec17..bc150705 100644 --- a/occupi-backend/tests/handlers_test.go +++ b/occupi-backend/tests/handlers_test.go @@ -24,6 +24,7 @@ import ( // "github.com/stretchr/testify/mock" ) +// Tests the ViewBookings handler func TestViewBookingsHandler(t *testing.T) { // connect to the database db := database.ConnectToDatabase(constants.AdminDBAccessOption) @@ -121,6 +122,7 @@ func TestViewBookingsHandler(t *testing.T) { } } +// Helper function to create a mock booking for testing func createMockBooking(r *gin.Engine, payload string, cookies []*http.Cookie) (map[string]interface{}, error) { req, err := http.NewRequest("POST", "/api/book-room", bytes.NewBuffer([]byte(payload))) if err != nil { @@ -149,7 +151,8 @@ func createMockBooking(r *gin.Engine, payload string, cookies []*http.Cookie) (m return response, nil } -func TestCancelBooking(t *testing.T) { +// SetupTestEnvironment initializes the test environment and returns the router and cookies +func SetupTestEnvironment(t *testing.T) (*gin.Engine, []*http.Cookie) { // Connect to the test database db := database.ConnectToDatabase(constants.AdminDBAccessOption) @@ -182,6 +185,14 @@ func TestCancelBooking(t *testing.T) { // Store the cookies from the login response cookies := req.Cookies() + return r, cookies +} + +// Tests the CancelBooking handler +func TestCancelBooking(t *testing.T) { + // Setup the test environment + r, cookies := SetupTestEnvironment(t) + // Define test cases testCases := []struct { name string @@ -297,6 +308,7 @@ func TestCancelBooking(t *testing.T) { } } +// Tests the BookRoom handler func TestBookRoom(t *testing.T) { // Connect to the test database db := database.ConnectToDatabase(constants.AdminDBAccessOption) @@ -407,6 +419,8 @@ func TestBookRoom(t *testing.T) { }) } } + +// Tests CheckIn handler func TestCheckIn(t *testing.T) { // Connect to the test database db := database.ConnectToDatabase(constants.AdminDBAccessOption) From 5559902ae65ceafcec9ed1acd818a7c441013582 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Sat, 29 Jun 2024 23:53:24 +0200 Subject: [PATCH 52/54] "Duplicates removed" --- occupi-backend/tests/handlers_test.go | 128 ++++++++------------------ 1 file changed, 39 insertions(+), 89 deletions(-) diff --git a/occupi-backend/tests/handlers_test.go b/occupi-backend/tests/handlers_test.go index bc150705..f4cda672 100644 --- a/occupi-backend/tests/handlers_test.go +++ b/occupi-backend/tests/handlers_test.go @@ -152,7 +152,7 @@ func createMockBooking(r *gin.Engine, payload string, cookies []*http.Cookie) (m } // SetupTestEnvironment initializes the test environment and returns the router and cookies -func SetupTestEnvironment(t *testing.T) (*gin.Engine, []*http.Cookie) { +func setupTestEnvironment(t *testing.T) (*gin.Engine, []*http.Cookie) { // Connect to the test database db := database.ConnectToDatabase(constants.AdminDBAccessOption) @@ -188,10 +188,43 @@ func SetupTestEnvironment(t *testing.T) (*gin.Engine, []*http.Cookie) { return r, cookies } +// Helper function to send a request and verify the response +func sendRequestAndVerifyResponse(t *testing.T, r *gin.Engine, method, url string, payload string, cookies []*http.Cookie, expectedStatusCode int, expectedMessage string) { + // Create a request to pass to the handler + req, err := http.NewRequest(method, url, bytes.NewBuffer([]byte(payload))) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Content-Type", "application/json") + + // Add the stored cookies to the request + for _, cookie := range cookies { + req.AddCookie(cookie) + } + + // Create a response recorder to record the response + rr := httptest.NewRecorder() + + // Serve the request + r.ServeHTTP(rr, req) + + // Check the status code is what we expect + assert.Equal(t, expectedStatusCode, rr.Code, "handler returned wrong status code") + + // Check the response message + var actualResponse map[string]interface{} + err = json.Unmarshal(rr.Body.Bytes(), &actualResponse) + if err != nil { + t.Fatalf("could not unmarshal response: %v", err) + } + + assert.Equal(t, expectedMessage, actualResponse["message"], "handler returned unexpected message") +} + // Tests the CancelBooking handler func TestCancelBooking(t *testing.T) { // Setup the test environment - r, cookies := SetupTestEnvironment(t) + r, cookies := setupTestEnvironment(t) // Define test cases testCases := []struct { @@ -276,34 +309,7 @@ func TestCancelBooking(t *testing.T) { tc.payload = strings.Replace(tc.payload, "mock_id", bookingID, 1) } - // Create a request to pass to the handler - req, err := http.NewRequest("POST", "/api/cancel-booking", bytes.NewBuffer([]byte(tc.payload))) - if err != nil { - t.Fatal(err) - } - - // Add the stored cookies to the request - for _, cookie := range cookies { - req.AddCookie(cookie) - } - - // Create a response recorder to record the response - rr := httptest.NewRecorder() - - // Serve the request - r.ServeHTTP(rr, req) - - // Check the status code is what we expect - assert.Equal(t, tc.expectedStatusCode, rr.Code, "handler returned wrong status code") - - // Check the response message - var actualResponse map[string]interface{} - err = json.Unmarshal(rr.Body.Bytes(), &actualResponse) - if err != nil { - t.Fatalf("could not unmarshal response: %v", err) - } - - assert.Equal(t, tc.expectedMessage, actualResponse["message"], "handler returned unexpected message") + sendRequestAndVerifyResponse(t, r, "POST", "/api/cancel-booking", tc.payload, cookies, tc.expectedStatusCode, tc.expectedMessage) }) } } @@ -422,37 +428,8 @@ func TestBookRoom(t *testing.T) { // Tests CheckIn handler func TestCheckIn(t *testing.T) { - // Connect to the test database - db := database.ConnectToDatabase(constants.AdminDBAccessOption) - - // Set Gin run mode - gin.SetMode(configs.GetGinRunMode()) - - // Create a Gin router - r := gin.Default() - - // Register the route - router.OccupiRouter(r, db) - - // Generate a token - token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) - - // Ping-auth test to ensure everything is set up correctly - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/ping-auth", nil) - req.AddCookie(&http.Cookie{Name: "token", Value: token}) - - r.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal( - t, - "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are auth'd\",\"status\":200}", - strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"), - ) - - // Store the cookies from the login response - cookies := req.Cookies() + // Setup the test environment + r, cookies := setupTestEnvironment(t) // Define test cases testCases := []struct { @@ -521,34 +498,7 @@ func TestCheckIn(t *testing.T) { tc.payload = strings.Replace(tc.payload, "mock_id", bookingID, 1) } - // Create a request to pass to the handler - req, err := http.NewRequest("POST", "/api/check-in", bytes.NewBuffer([]byte(tc.payload))) - if err != nil { - t.Fatal(err) - } - - // Add the stored cookies to the request - for _, cookie := range cookies { - req.AddCookie(cookie) - } - - // Create a response recorder to record the response - rr := httptest.NewRecorder() - - // Serve the request - r.ServeHTTP(rr, req) - - // Check the status code is what we expect - assert.Equal(t, tc.expectedStatusCode, rr.Code, "handler returned wrong status code") - - // Check the response message - var actualResponse map[string]interface{} - err = json.Unmarshal(rr.Body.Bytes(), &actualResponse) - if err != nil { - t.Fatalf("could not unmarshal response: %v", err) - } - - assert.Equal(t, tc.expectedMessage, actualResponse["message"], "handler returned unexpected message") + sendRequestAndVerifyResponse(t, r, "POST", "/api/check-in", tc.payload, cookies, tc.expectedStatusCode, tc.expectedMessage) }) } } From 53852e369a093f49aba73d9e18cb39d424ce3999 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Sun, 30 Jun 2024 00:04:59 +0200 Subject: [PATCH 53/54] "Can't remove certain test cases because they are testing different things" --- occupi-backend/tests/handlers_test.go | 39 ++++----------------------- 1 file changed, 5 insertions(+), 34 deletions(-) diff --git a/occupi-backend/tests/handlers_test.go b/occupi-backend/tests/handlers_test.go index f4cda672..5139bf76 100644 --- a/occupi-backend/tests/handlers_test.go +++ b/occupi-backend/tests/handlers_test.go @@ -302,11 +302,11 @@ func TestCancelBooking(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Setup the test case and get the booking ID if applicable - bookingID := tc.setupFunc() + id := tc.setupFunc() // Replace the mock_id placeholder in the payload with the actual booking ID - if bookingID != "" { - tc.payload = strings.Replace(tc.payload, "mock_id", bookingID, 1) + if id != "" { + tc.payload = strings.Replace(tc.payload, "mock_id", id, 1) } sendRequestAndVerifyResponse(t, r, "POST", "/api/cancel-booking", tc.payload, cookies, tc.expectedStatusCode, tc.expectedMessage) @@ -316,37 +316,8 @@ func TestCancelBooking(t *testing.T) { // Tests the BookRoom handler func TestBookRoom(t *testing.T) { - // Connect to the test database - db := database.ConnectToDatabase(constants.AdminDBAccessOption) - - // Set Gin run mode - gin.SetMode(configs.GetGinRunMode()) - - // Create a Gin router - r := gin.Default() - - // Register the route - router.OccupiRouter(r, db) - - // Generate a token - token, _, _ := authenticator.GenerateToken("test@example.com", constants.Basic) - - // Ping-auth test to ensure everything is set up correctly - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/ping-auth", nil) - req.AddCookie(&http.Cookie{Name: "token", Value: token}) - - r.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal( - t, - "{\"data\":null,\"message\":\"pong -> I am alive and kicking and you are auth'd\",\"status\":200}", - strings.ReplaceAll(w.Body.String(), "-\\u003e", "->"), - ) - - // Store the cookies from the login response - cookies := req.Cookies() + // Setup the test environment + r, cookies := setupTestEnvironment(t) // Define test cases testCases := []struct { From 3bb8b0bdc380f82572b16287855f44c44ce12776 Mon Sep 17 00:00:00 2001 From: Rethakgetse-Manaka Date: Sun, 30 Jun 2024 00:21:39 +0200 Subject: [PATCH 54/54] "Duplicates should be removed" --- occupi-backend/tests/handlers_test.go | 115 ++++++++++++++------------ 1 file changed, 60 insertions(+), 55 deletions(-) diff --git a/occupi-backend/tests/handlers_test.go b/occupi-backend/tests/handlers_test.go index 5139bf76..1dc0fd1f 100644 --- a/occupi-backend/tests/handlers_test.go +++ b/occupi-backend/tests/handlers_test.go @@ -122,6 +122,14 @@ func TestViewBookingsHandler(t *testing.T) { } } +type testCase struct { + name string + payload string + expectedStatusCode int + expectedMessage string + setupFunc func() string // Return booking ID for valid setup +} + // Helper function to create a mock booking for testing func createMockBooking(r *gin.Engine, payload string, cookies []*http.Cookie) (map[string]interface{}, error) { req, err := http.NewRequest("POST", "/api/book-room", bytes.NewBuffer([]byte(payload))) @@ -220,6 +228,57 @@ func sendRequestAndVerifyResponse(t *testing.T, r *gin.Engine, method, url strin assert.Equal(t, expectedMessage, actualResponse["message"], "handler returned unexpected message") } +func getSharedTestCases(r *gin.Engine, cookies []*http.Cookie) []testCase { + return []testCase{ + { + name: "Valid Request", + payload: `{ + "bookingId": "mock_id", + "creator": "test@example.com" + }`, + expectedStatusCode: http.StatusOK, + expectedMessage: "Successfully checked in!", + setupFunc: func() string { + // Insert a booking to be cancelled using the helper function + bookingPayload := `{ + "roomId": "12345", + "emails": ["test@example.com"], + "creator": "test@example.com", + "floorNo": "1", + "roomName": "Test Room", + "date": "2024-07-01T00:00:00Z", + "start": "2024-07-01T09:00:00Z", + "end": "2024-07-01T10:00:00Z" + }` + response, err := createMockBooking(r, bookingPayload, cookies) + if err != nil { + panic(fmt.Sprintf("could not create mock booking: %v", err)) + } + return response["data"].(string) // Assuming "data" contains the booking ID + }, + }, + { + name: "Invalid Request Payload", + payload: `{ + "bookingID": "", + "creator": "test@example.com" + }`, + expectedStatusCode: http.StatusBadRequest, + expectedMessage: "Invalid request payload", + setupFunc: func() string { return "" }, + }, + { + name: "Booking Not Found", + payload: `{ + "bookingId": "nonexistent", + "creator": "test@example.com" + }`, + expectedStatusCode: http.StatusNotFound, + expectedMessage: "Booking not found", + setupFunc: func() string { return "" }, + }, + } +} // Tests the CancelBooking handler func TestCancelBooking(t *testing.T) { @@ -403,61 +462,7 @@ func TestCheckIn(t *testing.T) { r, cookies := setupTestEnvironment(t) // Define test cases - testCases := []struct { - name string - payload string - expectedStatusCode int - expectedMessage string - setupFunc func() string // Return booking ID for valid setup - }{ - { - name: "Valid Request", - payload: `{ - "bookingId": "mock_id", - "creator": "test2@example.com" - }`, - expectedStatusCode: http.StatusOK, - expectedMessage: "Successfully checked in!", - setupFunc: func() string { - // Insert a booking to be cancelled using the helper function - bookingPayload := `{ - "roomId": "12345", - "emails": ["test2@example.com"], - "creator": "test2@example.com", - "floorNo": "1", - "roomName": "Test Room", - "date": "2024-07-01T00:00:00Z", - "start": "2024-07-01T09:00:00Z", - "end": "2024-07-01T10:00:00Z" - }` - response, err := createMockBooking(r, bookingPayload, cookies) - if err != nil { - t.Fatalf("could not create mock booking: %v", err) - } - return response["data"].(string) // Assuming "data" contains the booking ID - }, - }, - { - name: "Invalid Request Payload", - payload: `{ - "bookingID": "", - "creator": "test2@example.com" - }`, - expectedStatusCode: http.StatusBadRequest, - expectedMessage: "Invalid request payload", - setupFunc: func() string { return "" }, - }, - { - name: "Booking Not Found", - payload: `{ - "bookingId": "nonexistent", - "creator": "test2@example.com" - }`, - expectedStatusCode: http.StatusNotFound, - expectedMessage: "Booking not found", - setupFunc: func() string { return "" }, - }, - } + testCases := getSharedTestCases(r, cookies) for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) {