From 8b261ee90504c8af98402eb5c27bca8c17da07c5 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Tue, 28 Oct 2025 08:27:24 +0100 Subject: [PATCH 1/8] feat: Update user (host rooms) deletion and `repo.Delete` to perform logical deletion --- src/client/roomclient/client.go | 44 ++++++++++++++++++++++++++++++++- src/client/roomclient/model.go | 1 + src/domain/user.go | 1 + src/repo/repo.go | 7 +++--- src/service/service.go | 12 ++++++++- 5 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/client/roomclient/client.go b/src/client/roomclient/client.go index 4e3d2fa..cc53837 100644 --- a/src/client/roomclient/client.go +++ b/src/client/roomclient/client.go @@ -1,6 +1,16 @@ package roomclient +import ( + utils "bookem-user-service/util" + "context" + "encoding/json" + "fmt" + "io" + "net/http" +) + type RoomClient interface { + DeleteHostRooms(ctx context.Context, jwt string) ([]RoomDTO, error) } type roomClient struct { @@ -9,6 +19,38 @@ type roomClient struct { func NewRoomClient() RoomClient { return &roomClient{ - baseURL: "http://room-service:8080/api", // TODO: This should not be hardcoded + baseURL: "http://room-service:8080/api/", // TODO: This should not be hardcoded + } +} + +func (c *roomClient) DeleteHostRooms(ctx context.Context, jwt string) ([]RoomDTO, error) { + utils.TEL.Push(ctx, "delete-host-rooms") + defer utils.TEL.Pop() + + req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%shost/", c.baseURL), nil) + if err != nil { + utils.TEL.Error("preparing request error ", err) + return nil, err + } + req.Header.Add("Authorization", "Bearer "+jwt) + resp, err := http.DefaultClient.Do(req) + + if err != nil { + utils.TEL.Error("request error ", err) + return nil, err + } + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + utils.TEL.Error("parsing response error", err) + return nil, err } + + var obj []RoomDTO + if err := json.Unmarshal(bodyBytes, &obj); err != nil { + utils.TEL.Error("JSON unmarshall error", err) + return nil, err + } + + return obj, nil } diff --git a/src/client/roomclient/model.go b/src/client/roomclient/model.go index d2a4505..a66e2be 100644 --- a/src/client/roomclient/model.go +++ b/src/client/roomclient/model.go @@ -11,6 +11,7 @@ type RoomDTO struct { Photos []string `json:"photos"` Commodities []string `json:"commodities"` AutoApprove bool `json:"autoApprove"` + Deleted bool `json:"deleted"` } type CreateRoomDTO struct { diff --git a/src/domain/user.go b/src/domain/user.go index 067c867..7b628ac 100644 --- a/src/domain/user.go +++ b/src/domain/user.go @@ -17,4 +17,5 @@ type User struct { Surname string `json:"surname" gorm:"type:varchar(60);not null;"` Role UserRole `json:"role" gorm:"type:varchar(5);not null;check:role IN ('guest','host','admin')"` Address string `json:"address" gorm:"type:varchar(150);not null"` + Deleted bool `json:"deleted" gorm:"type:boolean;not null;default:false"` } diff --git a/src/repo/repo.go b/src/repo/repo.go index 545a524..8033065 100644 --- a/src/repo/repo.go +++ b/src/repo/repo.go @@ -12,7 +12,7 @@ type Repository interface { FindByUsernameOrEmailNotId(username, email string, id uint) (*domain.User, error) FindById(id uint) (*domain.User, error) Update(user *domain.User) error - Delete(id uint) + Delete(user *domain.User) error } type repository struct { @@ -58,6 +58,7 @@ func (r *repository) Update(user *domain.User) error { return r.db.Save(user).Error } -func (r *repository) Delete(id uint) { - r.db.Delete(&domain.User{}, id) +func (r *repository) Delete(user *domain.User) error { + user.Deleted = true + return r.Update(user) } diff --git a/src/service/service.go b/src/service/service.go index 7763b36..d30bde3 100644 --- a/src/service/service.go +++ b/src/service/service.go @@ -315,12 +315,22 @@ func (s *service) Delete(ctx context.Context, userId uint, jwt string) error { return err } + // Delete rooms + + if user.Role == domain.Host { + _, err := s.roomClient.DeleteHostRooms(ctx, jwt) + if err != nil { + util.TEL.Error("could not delete host rooms", err) + return err + } + } + // Delete user util.TEL.Push(ctx, "delete-user-in-db") defer util.TEL.Pop() - s.repo.Delete(user.ID) + s.repo.Delete(user) util.TEL.Info("User deleted", "id", userId) return nil From 67d08275f4cac9c4e1a1646f1c057db9ed79ee8c Mon Sep 17 00:00:00 2001 From: Vasilije Date: Tue, 28 Oct 2025 08:29:45 +0100 Subject: [PATCH 2/8] feat: Verify if user account is deleted on login --- src/service/service.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/service/service.go b/src/service/service.go index d30bde3..0bd5821 100644 --- a/src/service/service.go +++ b/src/service/service.go @@ -96,6 +96,13 @@ func (s *service) Login(ctx context.Context, dto domain.LoginDTO) (string, error return "", domain.ErrLoginFailed } + util.TEL.Push(ctx, "check-account-deletion") + defer util.TEL.Pop() + if user.Deleted { + util.TEL.Error("account is deleted", nil, "username_or_email", dto.UsernameOrEmail) + return "", domain.ErrDeletedAccount + } + util.TEL.Push(ctx, "verify-password") defer util.TEL.Pop() From a24bf95eabf56cf76c78b61ffa767b5d82ae7e21 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Tue, 28 Oct 2025 08:31:03 +0100 Subject: [PATCH 3/8] feat: Retune terminology --- src/domain/error.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/domain/error.go b/src/domain/error.go index c484cb8..a901327 100644 --- a/src/domain/error.go +++ b/src/domain/error.go @@ -9,6 +9,7 @@ var ( ErrDBInternal = errors.New("database internal error") ErrInvalidInput = errors.New("invalid input") ErrLoginFailed = errors.New("invalid user or password") + ErrDeletedAccount = errors.New("deleted account") ErrUnauthorized = errors.New("unauthorized") ErrPasswordsNotMatch = errors.New("confirm password does not match") ErrPasswordNotChanged = errors.New("password must be different") @@ -16,8 +17,8 @@ var ( ErrNotFound = errors.New("not found") ErrWrongPassword = errors.New("incorrect password") - ErrGuestHasReservations = errors.New("user has pending reservations") - ErrHostHasReservations = errors.New("user has room(s) with pending reservations") + ErrGuestHasReservations = errors.New("user has active reservations") + ErrHostHasReservations = errors.New("user has room(s) with active reservations") ErrCannotDeleteAdmin = errors.New("admin accounts cannot be deleted") ) From 0515057afeb6dadfa8c301cc566ef72a507f4524 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Tue, 28 Oct 2025 08:35:37 +0100 Subject: [PATCH 4/8] feat: Update unit tests for `Delete` of user --- src/test/unit/delete_test.go | 14 ++++++++------ src/test/unit/utils.go | 12 ++++++++++-- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/test/unit/delete_test.go b/src/test/unit/delete_test.go index 564df42..6097244 100644 --- a/src/test/unit/delete_test.go +++ b/src/test/unit/delete_test.go @@ -2,6 +2,7 @@ package test import ( "bookem-user-service/client/reservationclient" + "bookem-user-service/client/roomclient" domain "bookem-user-service/domain" "context" "fmt" @@ -20,7 +21,7 @@ func TestDelete_GuestSuccess(t *testing.T) { user.Role = domain.Guest mockRepo.On("FindById", id).Return(user, nil) - mockRepo.On("Delete", id).Return() + mockRepo.On("Delete", user).Return(nil) mockReservationClient.On("GetActiveGuestReservations", context.Background(), jwt).Return([]reservationclient.ReservationDTO{}, nil) err := svc.Delete(context.Background(), id, jwt) @@ -29,7 +30,7 @@ func TestDelete_GuestSuccess(t *testing.T) { } func TestDelete_HostSuccess(t *testing.T) { - svc, mockRepo, _, mockReservationClient := createTestService() + svc, mockRepo, mockRoomClient, mockReservationClient := createTestService() id := uint(1) jwt := "token" @@ -38,8 +39,9 @@ func TestDelete_HostSuccess(t *testing.T) { user.Role = domain.Host mockRepo.On("FindById", id).Return(user, nil) - mockRepo.On("Delete", id).Return() + mockRepo.On("Delete", user).Return(nil) mockReservationClient.On("GetActiveHostReservations", context.Background(), jwt).Return([]reservationclient.ReservationDTO{}, nil) + mockRoomClient.On("DeleteHostRooms", context.Background(), jwt).Return([]roomclient.RoomDTO{}, nil) err := svc.Delete(context.Background(), id, jwt) @@ -68,7 +70,7 @@ func TestDelete_GuestHasActiveReservations(t *testing.T) { user.ID = id user.Role = domain.Guest mockRepo.On("FindById", id).Return(user, nil) - mockRepo.On("Delete", id).Return() + mockRepo.On("Delete", id).Return(nil) reservation := reservationclient.ReservationDTO{} mockReservationClient.On("GetActiveGuestReservations", context.Background(), jwt).Return([]reservationclient.ReservationDTO{reservation}, nil) @@ -87,7 +89,7 @@ func TestDelete_HostHasActiveReservations(t *testing.T) { user.ID = id user.Role = domain.Host mockRepo.On("FindById", id).Return(user, nil) - mockRepo.On("Delete", id).Return() + mockRepo.On("Delete", id).Return(nil) reservation := reservationclient.ReservationDTO{} mockReservationClient.On("GetActiveHostReservations", context.Background(), jwt).Return([]reservationclient.ReservationDTO{reservation}, nil) @@ -106,7 +108,7 @@ func TestDelete_TriedDeletingAdmin(t *testing.T) { user.ID = id user.Role = domain.Admin mockRepo.On("FindById", id).Return(user, nil) - mockRepo.On("Delete", id).Return() + mockRepo.On("Delete", id).Return(nil) err := svc.Delete(context.Background(), id, jwt) diff --git a/src/test/unit/utils.go b/src/test/unit/utils.go index 6d74a95..ae704f3 100644 --- a/src/test/unit/utils.go +++ b/src/test/unit/utils.go @@ -2,6 +2,7 @@ package test import ( "bookem-user-service/client/reservationclient" + "bookem-user-service/client/roomclient" domain "bookem-user-service/domain" service "bookem-user-service/service" "context" @@ -55,8 +56,9 @@ func (m *MockRepo) Update(user *domain.User) error { return args.Error(0) } -func (m *MockRepo) Delete(id uint) { - m.Called(id) +func (m *MockRepo) Delete(user *domain.User) error { + args := m.Called(user) + return args.Error(0) } // ---------------------------------------------- Mock room client @@ -65,6 +67,12 @@ type MockRoomClient struct { mock.Mock } +func (m *MockRoomClient) DeleteHostRooms(ctx context.Context, jwt string) ([]roomclient.RoomDTO, error) { + args := m.Called(ctx, jwt) + rooms, _ := args.Get(0).([]roomclient.RoomDTO) + return rooms, args.Error(1) +} + // ---------------------------------------------- Mock reservation client type MockReservationClient struct { From d1c280c23dde2f3183d4f9e7491e041c9dce2ee2 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Tue, 28 Oct 2025 08:37:31 +0100 Subject: [PATCH 5/8] fix: Add missing error handling for `login` --- src/api/middleware/error_middleware.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api/middleware/error_middleware.go b/src/api/middleware/error_middleware.go index d530e24..9bd52f8 100644 --- a/src/api/middleware/error_middleware.go +++ b/src/api/middleware/error_middleware.go @@ -31,6 +31,8 @@ func mapErrorToStatus(err error) int { return http.StatusNotFound case errors.Is(err, domain.ErrWrongPassword): return http.StatusUnauthorized + case errors.Is(err, domain.ErrDeletedAccount): + return http.StatusUnauthorized default: return http.StatusInternalServerError } From 022d89a386e7286c52b066a6ed8d48624d992d58 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Tue, 28 Oct 2025 08:39:09 +0100 Subject: [PATCH 6/8] feat: Add JWT utility for integration tests --- compose.integration.yml | 3 ++ src/api/middleware/jwt.go | 44 ++++++++++++++++++++++++++++ src/api/middleware/jwt_middleware.go | 15 ++++++++++ 3 files changed, 62 insertions(+) create mode 100644 src/api/middleware/jwt.go diff --git a/compose.integration.yml b/compose.integration.yml index ff29095..3229ea6 100644 --- a/compose.integration.yml +++ b/compose.integration.yml @@ -18,8 +18,11 @@ services: condition: service_healthy reservation-service: condition: service_healthy + environment: + JWT_PUBLIC_KEY_PATH: /app/keys/public_key.pem volumes: - go-mod-cache:/go/pkg/mod + - ${USER_SERVICE_PATH}/keys/public_key.pem:/app/keys/public_key.pem:ro user-service: build: diff --git a/src/api/middleware/jwt.go b/src/api/middleware/jwt.go new file mode 100644 index 0000000..0075dc7 --- /dev/null +++ b/src/api/middleware/jwt.go @@ -0,0 +1,44 @@ +package middleware + +import ( + "fmt" + "os" + + "github.com/golang-jwt/jwt/v5" +) + +var JWT_PUBLIC_KEY_PATH = os.Getenv("JWT_PUBLIC_KEY_PATH") + +var ParseJWT = parseJWT + +// ParseJWT validates and extracts claims from an encoded JWT string. +func parseJWT(tokenString string) (jwt.MapClaims, error) { + publicKeyData, err := os.ReadFile(JWT_PUBLIC_KEY_PATH) + if err != nil { + return nil, fmt.Errorf("could not open public key %s: %w", JWT_PUBLIC_KEY_PATH, err) + } + + publicKey, err := jwt.ParseRSAPublicKeyFromPEM(publicKeyData) + if err != nil { + return nil, fmt.Errorf("could not parse public key: %w", err) + } + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method") + } + return publicKey, nil + }) + + if err != nil { + return nil, err + } + + claims, ok := token.Claims.(jwt.MapClaims) + + if !ok { + return nil, fmt.Errorf("invalid jwt token or claims") + } + + return claims, nil +} diff --git a/src/api/middleware/jwt_middleware.go b/src/api/middleware/jwt_middleware.go index 6b29357..527a234 100644 --- a/src/api/middleware/jwt_middleware.go +++ b/src/api/middleware/jwt_middleware.go @@ -63,3 +63,18 @@ func GetJwt(ctx *gin.Context) (*Jwt, error) { return &jwt, nil } + +func GetJwtFromString(jwtString string) (*Jwt, error) { + jwtData, err := ParseJWT(jwtString) + if err != nil { + return nil, err + } + + jwt := Jwt{ + ID: uint(jwtData["sub"].(float64)), + Username: jwtData["username"].(string), + Role: domain.UserRole(jwtData["role"].(string)), + } + + return &jwt, nil +} From ad24243b4a8162151885ccefb7e7cc019ffae6aa Mon Sep 17 00:00:00 2001 From: Vasilije Date: Tue, 28 Oct 2025 08:41:56 +0100 Subject: [PATCH 7/8] test: Update integration tests for user deletion --- src/client/reservationclient/model.go | 19 ++ src/client/roomclient/model.go | 40 ++++ src/domain/dto.go | 2 + .../integration/delete_integration_test.go | 100 +++++++--- src/test/integration/util.go | 176 ++++++++++++++++++ 5 files changed, 311 insertions(+), 26 deletions(-) diff --git a/src/client/reservationclient/model.go b/src/client/reservationclient/model.go index 7aa16d0..a752013 100644 --- a/src/client/reservationclient/model.go +++ b/src/client/reservationclient/model.go @@ -14,3 +14,22 @@ type ReservationDTO struct { Cancelled bool `gorm:"not null"` Cost uint `gorm:"not null"` } + +type CreateReservationRequestDTO struct { + RoomID uint `json:"roomId"` + DateFrom time.Time `json:"dateFrom"` + DateTo time.Time `json:"dateTo"` + GuestCount uint `json:"guestCount"` +} + +type ReservationRequestDTO struct { + ID uint `json:"id"` + RoomID uint `json:"roomId"` + DateFrom time.Time `json:"dateFrom"` + DateTo time.Time `json:"dateTo"` + GuestCount uint `json:"guestCount"` + GuestID uint `json:"guestId"` + Status string `json:"status"` + Cost uint `json:"cost"` + GuestCancelCount uint `json:"guestCancelCount"` +} diff --git a/src/client/roomclient/model.go b/src/client/roomclient/model.go index a66e2be..9e4529b 100644 --- a/src/client/roomclient/model.go +++ b/src/client/roomclient/model.go @@ -1,5 +1,7 @@ package roomclient +import "time" + type RoomDTO struct { ID uint `json:"id"` HostID uint `json:"hostID"` @@ -25,3 +27,41 @@ type CreateRoomDTO struct { Commodities []string `json:"commodities"` AutoApprove bool `json:"autoApprove"` } + +// --------------------------------------------------------------- + +type CreateRoomAvailabilityListDTO struct { + RoomID uint `json:"roomId"` + Items []CreateRoomAvailabilityItemDTO `json:"items"` +} + +type CreateRoomAvailabilityItemDTO struct { + // ExistingID is either the ID of an RoomAvailabilityItem that already + // exists, or 0 if this is a new item. When 0, a new one will be created in + // the DB. When not 0, it will reuse the existing object. + ExistingID uint `json:"existingId"` + DateFrom time.Time `json:"dateFrom"` + DateTo time.Time `json:"dateTo"` + Available bool `json:"available"` +} + +// --------------------------------------------------------------- + +type CreateRoomPriceListDTO struct { + RoomID uint `json:"roomId"` + Items []CreateRoomPriceItemDTO `json:"items"` + BasePrice uint `json:"basePrice"` + PerGuest bool `json:"perGuest"` +} + +type CreateRoomPriceItemDTO struct { + // ExistingID is either the ID of an RoomPriceItem that already + // exists, or 0 if this is a new item. When 0, a new one will be created in + // the DB. When not 0, it will reuse the existing object. + ExistingID uint `json:"existingId"` + DateFrom time.Time `json:"dateFrom"` + DateTo time.Time `json:"dateTo"` + Price uint `json:"price"` +} + +// --------------------------------------------------------------- diff --git a/src/domain/dto.go b/src/domain/dto.go index 1065f6a..edf4876 100644 --- a/src/domain/dto.go +++ b/src/domain/dto.go @@ -18,6 +18,7 @@ type UserDTO struct { Surname string `json:"surname" ` Address string `json:"address" ` Role string `json:"role" ` + Deleted bool `json:"deleted" ` } type UserUpdateDTO struct { @@ -38,6 +39,7 @@ func NewUserDTO(user *User) UserDTO { Surname: user.Surname, Address: user.Address, Role: string(user.Role), + Deleted: user.Deleted, } } diff --git a/src/test/integration/delete_integration_test.go b/src/test/integration/delete_integration_test.go index 559b7cd..9e71c38 100644 --- a/src/test/integration/delete_integration_test.go +++ b/src/test/integration/delete_integration_test.go @@ -1,25 +1,28 @@ package test import ( + "bookem-user-service/client/reservationclient" "bookem-user-service/domain" "net/http" + "strconv" "testing" + "time" "github.com/stretchr/testify/require" ) func TestIntegration_Delete(t *testing.T) { { - resp, _ := registerUser("guest1", "1234", domain.Guest) - jwt := loginUser2("guest1", "1234") + resp, _ := registerUser("guest_idel_1", "1234", domain.Guest) + jwt := loginUser2("guest_idel_1", "1234") resp, err := deleteUser(jwt) require.Nil(t, err) require.Equal(t, http.StatusNoContent, resp.StatusCode) } { - resp, _ := registerUser("host1", "1234", domain.Host) - jwt := loginUser2("host1", "1234") + resp, _ := registerUser("host_idel_1", "1234", domain.Host) + jwt := loginUser2("host_idel_1", "1234") resp, err := deleteUser(jwt) require.Nil(t, err) @@ -28,46 +31,91 @@ func TestIntegration_Delete(t *testing.T) { } func TestIntegration_Delete_GuestHasActiveReservations(t *testing.T) { - resp, _ := registerUser("guest2", "1234", domain.Guest) - jwt := loginUser2("guest2", "1234") + hostUsername := "host_idel_2" + _, _, hostJwt, room := setupHostRoomAvailabilityPrice(hostUsername, t) - // TODO: Once we can actually create reservations - // we will add active reservations here. - // Until then, this test will pass. + registerUser("guest_idel_2", "1234", domain.Guest) + guestJwt := loginUser2("guest_idel_2", "1234") - resp, err := deleteUser(jwt) + dto := reservationclient.CreateReservationRequestDTO{ + RoomID: room.ID, + DateFrom: time.Date(2025, 9, 6, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 8, 0, 0, 0, 0, time.UTC), + GuestCount: 2, + } + + resp, err := createReservationRequest(guestJwt, dto) + require.NoError(t, err) + require.Equal(t, http.StatusCreated, resp.StatusCode) + req := responseToReservationRequest(resp) + + approveURL := URL_reservation + "req/" + strconv.FormatUint(uint64(req.ID), 10) + "/approve" + request, err := http.NewRequest(http.MethodPut, approveURL, nil) + require.NoError(t, err) + request.Header.Add("Authorization", "Bearer "+hostJwt) + + approveResp, err := http.DefaultClient.Do(request) + require.NoError(t, err) + require.Equal(t, http.StatusOK, approveResp.StatusCode) + + resp, err = deleteUser(hostJwt) require.Nil(t, err) require.Equal(t, http.StatusNoContent, resp.StatusCode) } func TestIntegration_Delete_GuestHasNoActiveReservations(t *testing.T) { - resp, _ := registerUser("guest3", "1234", domain.Guest) - jwt := loginUser2("guest3", "1234") + resp, _ := registerUser("guest_idel_3", "1234", domain.Guest) + guestJwt := loginUser2("guest_idel_3", "1234") + + hostUsername := "host_idel_3" + _, _, _, room := setupHostRoomAvailabilityPrice(hostUsername, t) + + // This reservation request is inactive. + dto := reservationclient.CreateReservationRequestDTO{ + RoomID: room.ID, + DateFrom: time.Date(2025, 9, 6, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 8, 0, 0, 0, 0, time.UTC), + GuestCount: 2, + } - // TODO: Once we can actually create reservations - // we will add non-active reservations here. - // Until then, this test will pass. + resp, err := createReservationRequest(guestJwt, dto) + require.NoError(t, err) + require.Equal(t, http.StatusCreated, resp.StatusCode) - resp, err := deleteUser(jwt) + resp, err = deleteUser(guestJwt) require.Nil(t, err) require.Equal(t, http.StatusNoContent, resp.StatusCode) } func TestIntegration_Delete_HostHasActiveReservations(t *testing.T) { - resp, _ := registerUser("host2", "1234", domain.Host) - id := getUserFromRegister(resp).Id - jwt := loginUser2("host2", "1234") + hostUsername := "host_idel_4" + _, _, hostJwt, room := setupHostRoomAvailabilityPrice(hostUsername, t) + + registerUser("guest_idel_4", "1234", domain.Guest) + guestJwt := loginUser2("guest_idel_4", "1234") - roomDTO := DefaultRoomCreateDTO - roomDTO.HostID = id + dto := reservationclient.CreateReservationRequestDTO{ + RoomID: room.ID, + DateFrom: time.Date(2025, 9, 6, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 8, 0, 0, 0, 0, time.UTC), + GuestCount: 2, + } - resp, _ = createRoom(jwt, roomDTO) + resp, err := createReservationRequest(guestJwt, dto) + require.NoError(t, err) + require.Equal(t, http.StatusCreated, resp.StatusCode) + req := responseToReservationRequest(resp) - // TODO: Once we can actually create reservations - // we will add active reservations here. - // Until then, this test will pass. + approveURL := URL_reservation + "req/" + strconv.FormatUint(uint64(req.ID), 10) + "/approve" + request, err := http.NewRequest(http.MethodPut, approveURL, nil) + require.NoError(t, err) + request.Header.Add("Authorization", "Bearer "+hostJwt) - resp, err := deleteUser(jwt) + approveResp, err := http.DefaultClient.Do(request) + require.NoError(t, err) + require.Equal(t, http.StatusOK, approveResp.StatusCode) + + resp, err = deleteUser(hostJwt) require.Nil(t, err) require.Equal(t, http.StatusNoContent, resp.StatusCode) } diff --git a/src/test/integration/util.go b/src/test/integration/util.go index d5fcab5..3f2fed6 100644 --- a/src/test/integration/util.go +++ b/src/test/integration/util.go @@ -1,6 +1,8 @@ package test import ( + middleware "bookem-user-service/api/middleware" + "bookem-user-service/client/reservationclient" "bookem-user-service/client/roomclient" "bookem-user-service/domain" "bytes" @@ -10,6 +12,10 @@ import ( "math/rand" "net/http" "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" ) const URL = "http://user-service:8080/api/" @@ -173,6 +179,176 @@ func createRoom(jwt string, dto roomclient.CreateRoomDTO) (*http.Response, error return http.DefaultClient.Do(req) } +func createReservationRequest(jwt string, dto reservationclient.CreateReservationRequestDTO) (*http.Response, error) { + jsonBytes, err := json.Marshal(dto) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, URL_reservation+"req", bytes.NewBuffer(jsonBytes)) + if err != nil { + return nil, err + } + req.Header.Add("Authorization", "Bearer "+jwt) + return http.DefaultClient.Do(req) +} + +func responseToReservationRequest(resp *http.Response) reservationclient.ReservationRequestDTO { + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + panic(fmt.Sprintf("failed to read response body: %v", err)) + } + + var obj reservationclient.ReservationRequestDTO + if err := json.Unmarshal(bodyBytes, &obj); err != nil { + panic(fmt.Sprintf("failed to unmarshal: %v", err)) + } + + return obj +} + +func responseToRoom(resp *http.Response) roomclient.RoomDTO { + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + panic(fmt.Sprintf("failed to read response body: %v", err)) + } + + var obj roomclient.RoomDTO + if err := json.Unmarshal(bodyBytes, &obj); err != nil { + fmt.Print(string(bodyBytes)) + panic(fmt.Sprintf("failed to unmarshal: %v", err)) + } + + return obj +} + +func createRoomAvailability(jwt string, dto roomclient.CreateRoomAvailabilityListDTO) (*http.Response, error) { + jsonBytes, err := json.Marshal(dto) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, URL_room+"available", bytes.NewBuffer(jsonBytes)) + if err != nil { + return nil, err + } + req.Header.Add("Authorization", "Bearer "+jwt) + return http.DefaultClient.Do(req) +} + +func createRoomPrice(jwt string, dto roomclient.CreateRoomPriceListDTO) (*http.Response, error) { + jsonBytes, err := json.Marshal(dto) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, URL_room+"price", bytes.NewBuffer(jsonBytes)) + if err != nil { + return nil, err + } + req.Header.Add("Authorization", "Bearer "+jwt) + return http.DefaultClient.Do(req) +} + +func setupHostRoomAvailabilityPrice(hostUsername string, t *testing.T) (string, string, string, roomclient.RoomDTO) { + // Step 1: Register unique host + username := hostUsername + password := "pass" + registerUser(username, password, domain.Host) + jwt := loginUser2(username, password) + jwtObj, err := middleware.GetJwtFromString(jwt) + require.NoError(t, err) + + // Step 2: Create room + roomDTO := roomclient.CreateRoomDTO{ + HostID: jwtObj.ID, + Name: "Room_" + genName(6), + Description: "Test room", + Address: "Test address", + MinGuests: 1, + MaxGuests: 4, + PhotosPayload: []string{SMALL_IMG}, + Commodities: []string{"WiFi", "AC"}, + AutoApprove: false, + } + roomResp, err := createRoom(jwt, roomDTO) + require.NoError(t, err) + defer roomResp.Body.Close() + room := responseToRoom(roomResp) + + // Step 3: Create availability list + availabilityDTO := roomclient.CreateRoomAvailabilityListDTO{ + RoomID: room.ID, + Items: []roomclient.CreateRoomAvailabilityItemDTO{ + { + ExistingID: 0, + DateFrom: time.Date(2025, 9, 1, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 10, 0, 0, 0, 0, time.UTC), + Available: true, + }, + { + ExistingID: 0, + DateFrom: time.Date(2025, 9, 15, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 20, 0, 0, 0, 0, time.UTC), + Available: true, + }, + { + ExistingID: 0, + DateFrom: time.Date(2025, 9, 22, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 30, 0, 0, 0, 0, time.UTC), + Available: true, + }, + { + ExistingID: 0, + DateFrom: time.Date(2025, 12, 1, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 12, 10, 0, 0, 0, 0, time.UTC), + Available: true, + }, + }, + } + availResp, err := createRoomAvailability(jwt, availabilityDTO) + require.NoError(t, err) + defer availResp.Body.Close() + + // Step 4: Create price list + priceDTO := roomclient.CreateRoomPriceListDTO{ + RoomID: room.ID, + BasePrice: 80, + PerGuest: false, + Items: []roomclient.CreateRoomPriceItemDTO{ + { + ExistingID: 0, + DateFrom: time.Date(2025, 9, 1, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 10, 0, 0, 0, 0, time.UTC), + Price: 100, + }, + { + ExistingID: 0, + DateFrom: time.Date(2025, 9, 15, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 20, 0, 0, 0, 0, time.UTC), + Price: 120, + }, + { + ExistingID: 0, + DateFrom: time.Date(2025, 9, 22, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 30, 0, 0, 0, 0, time.UTC), + Price: 200, + }, + { + ExistingID: 0, + DateFrom: time.Date(2025, 12, 1, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 12, 10, 0, 0, 0, 0, time.UTC), + Price: 200, + }, + }, + } + priceResp, err := createRoomPrice(jwt, priceDTO) + require.NoError(t, err) + defer priceResp.Body.Close() + + return username, password, jwt, room +} + // ----------------------------------------------- Mock data const ( From 46875e58b5afce3b36b69c493213432bd428a3ff Mon Sep 17 00:00:00 2001 From: Vasilije Date: Tue, 28 Oct 2025 09:19:03 +0100 Subject: [PATCH 8/8] fix: Use latest changes during CI integration testing --- ci.override.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci.override.env b/ci.override.env index 7261b24..e86aab0 100644 --- a/ci.override.env +++ b/ci.override.env @@ -1,7 +1,7 @@ RESERVATION_SERVICE_PATH=./services/reservation-service RESERVATION_SERVICE_REPO=https://github.com/book-em/reservation-service.git -RESERVATION_SERVICE_BRANCH=develop +RESERVATION_SERVICE_BRANCH=feature-delete-user-complete ROOM_SERVICE_PATH=./services/room-service ROOM_SERVICE_REPO=https://github.com/book-em/room-service.git -ROOM_SERVICE_BRANCH=develop \ No newline at end of file +ROOM_SERVICE_BRANCH=feature-delete-user-complete \ No newline at end of file