diff --git a/internal/service/rooms/occupants.go b/internal/service/rooms/occupants.go index 3c4d488..1df208f 100644 --- a/internal/service/rooms/occupants.go +++ b/internal/service/rooms/occupants.go @@ -83,7 +83,7 @@ func (r *roomService) RemoveOccupantFromRoom(ctx context.Context, roomID string, } if room.ID != existingMembership.RoomID { - return common.NewConflict(ctx, common.RoomOccupantConflict, common.Details("this attendee is not in this room")) + return common.NewConflict(ctx, common.RoomOccupantConflict, common.Details("this attendee is in a different room")) } if err := r.DB.DeleteRoomMembership(ctx, badgeNumber); err != nil { diff --git a/test/acceptance/rooms_create_test.go b/test/acceptance/rooms_create_test.go index 380506a..633eb0f 100644 --- a/test/acceptance/rooms_create_test.go +++ b/test/acceptance/rooms_create_test.go @@ -143,3 +143,5 @@ func TestRoomsCreate_NameTooLong(t *testing.T) { docs.Then("Then the request fails with the expected error") tstRequireErrorResponse(t, response, http.StatusBadRequest, "room.data.invalid", url.Values{"name": []string{"room name too long, max 50 characters"}}) } + +// TODO duplicate name diff --git a/test/acceptance/rooms_occupant_add_test.go b/test/acceptance/rooms_occupant_add_test.go index fb846a5..d09f025 100644 --- a/test/acceptance/rooms_occupant_add_test.go +++ b/test/acceptance/rooms_occupant_add_test.go @@ -1,6 +1,7 @@ package acceptance import ( + "fmt" "github.com/eurofurence/reg-room-service/docs" "github.com/eurofurence/reg-room-service/internal/repository/downstreams/attendeeservice" "github.com/stretchr/testify/require" @@ -144,7 +145,55 @@ func TestRoomsAddOccupant_Full(t *testing.T) { tstRoomState(t, location, squirrel, snep) } -func TestRomsAddOccupant_RoomNotFound(t *testing.T) { +func TestRoomsAddOccupant_Duplicate(t *testing.T) { + tstSetup(tstDefaultConfigFileRoomGroups) + defer tstShutdown() + + docs.Given("Given a room with an occupied bed") + location := setupExistingRoom(t, "31415", squirrel) + occupantLoc := fmt.Sprintf("%s/occupants/%d", location, squirrel.ID) + + docs.Given("Given an attendee with an active registration who is already in the room") + registerSubject(subject(squirrel)) + + docs.Given("Given an admin") + token := tstValidAdminToken(t) + + docs.When("When the admin tries to add the attendee to the room again") + response := tstPerformPostNoBody(occupantLoc, token) + + docs.Then("Then the request fails with the expected error") + tstRequireErrorResponse(t, response, http.StatusConflict, "room.occupant.duplicate", "this attendee is already in this room") + + docs.Then("And the room is unchanged") + tstRoomState(t, location, squirrel) +} + +func TestRoomsAddOccupant_InAnotherRoom(t *testing.T) { + tstSetup(tstDefaultConfigFileRoomGroups) + defer tstShutdown() + + docs.Given("Given a room with free beds") + location := setupExistingRoom(t, "31415") + occupantLoc := fmt.Sprintf("%s/occupants/%d", location, squirrel.ID) + + docs.Given("Given an attendee who is already in another room") + _ = setupExistingRoom(t, "27182", squirrel) + + docs.Given("Given an admin") + token := tstValidAdminToken(t) + + docs.When("When the admin tries to also add the attendee to the first room") + response := tstPerformPostNoBody(occupantLoc, token) + + docs.Then("Then the request fails with the expected error") + tstRequireErrorResponse(t, response, http.StatusConflict, "room.occupant.conflict", "this attendee is already in another room") + + docs.Then("And the room is unchanged") + tstRoomState(t, location) +} + +func TestRoomsAddOccupant_RoomNotFound(t *testing.T) { tstSetup(tstDefaultConfigFileRoomGroups) defer tstShutdown() @@ -161,7 +210,7 @@ func TestRomsAddOccupant_RoomNotFound(t *testing.T) { tstRequireErrorResponse(t, response, http.StatusNotFound, "room.id.notfound", "room does not exist") } -func TestRomsAddOccupant_AttendeeNotFound(t *testing.T) { +func TestRoomsAddOccupant_AttendeeNotFound(t *testing.T) { tstSetup(tstDefaultConfigFileRoomGroups) defer tstShutdown() @@ -178,7 +227,7 @@ func TestRomsAddOccupant_AttendeeNotFound(t *testing.T) { tstRequireErrorResponse(t, response, http.StatusNotFound, "attendee.notfound", "no such attendee") } -func TestRomsAddOccupant_AttendeeNotAttending(t *testing.T) { +func TestRoomsAddOccupant_AttendeeNotAttending(t *testing.T) { tstSetup(tstDefaultConfigFileRoomGroups) defer tstShutdown() @@ -201,7 +250,7 @@ func TestRomsAddOccupant_AttendeeNotAttending(t *testing.T) { tstRoomState(t, location, snep) } -func TestRomsAddOccupant_InvalidRoomID(t *testing.T) { +func TestRoomsAddOccupant_InvalidRoomID(t *testing.T) { tstSetup(tstDefaultConfigFileRoomGroups) defer tstShutdown() @@ -218,7 +267,7 @@ func TestRomsAddOccupant_InvalidRoomID(t *testing.T) { tstRequireErrorResponse(t, response, http.StatusBadRequest, "room.id.invalid", "you must specify a valid uuid") } -func TestRomsAddOccupant_BadgeNumberInvalid(t *testing.T) { +func TestRoomsAddOccupant_BadgeNumberInvalid(t *testing.T) { tstSetup(tstDefaultConfigFileRoomGroups) defer tstShutdown() @@ -235,7 +284,7 @@ func TestRomsAddOccupant_BadgeNumberInvalid(t *testing.T) { tstRequireErrorResponse(t, response, http.StatusBadRequest, "request.parse.failed", "invalid badge number - must be positive integer") } -func TestRomsAddOccupant_BadgeNumberNegative(t *testing.T) { +func TestRoomsAddOccupant_BadgeNumberNegative(t *testing.T) { tstSetup(tstDefaultConfigFileRoomGroups) defer tstShutdown() diff --git a/test/acceptance/rooms_occupant_remove_test.go b/test/acceptance/rooms_occupant_remove_test.go index d3c3d83..eb754a5 100644 --- a/test/acceptance/rooms_occupant_remove_test.go +++ b/test/acceptance/rooms_occupant_remove_test.go @@ -1,3 +1,263 @@ package acceptance +import ( + "fmt" + "github.com/eurofurence/reg-room-service/docs" + "github.com/eurofurence/reg-room-service/internal/repository/downstreams/attendeeservice" + "github.com/stretchr/testify/require" + "net/http" + "testing" +) +func TestRoomsRemoveOccupant_NotLoggedIn(t *testing.T) { + tstSetup(tstDefaultConfigFileRoomGroups) + defer tstShutdown() + + docs.Given("Given a room with an occupied bed") + location := setupExistingRoom(t, "31415", squirrel) + occupantLoc := fmt.Sprintf("%s/occupants/%d", location, squirrel.ID) + + docs.Given("Given an anonymous user") + token := tstNoToken() + + docs.When("When they try to remove the attendee from the room") + response := tstPerformDelete(occupantLoc, token) + + docs.Then("Then the request is denied") + tstRequireErrorResponse(t, response, http.StatusUnauthorized, "auth.unauthorized", "you must be logged in for this operation") + + docs.Then("And the room is unchanged") + tstRoomState(t, location, squirrel) +} + +func TestRoomsRemoveOccupant_UserDenyOtherRemove(t *testing.T) { + tstSetup(tstDefaultConfigFileRoomGroups) + defer tstShutdown() + + docs.Given("Given a room with an occupied bed") + location := setupExistingRoom(t, "31415", snep) + occupantLoc := fmt.Sprintf("%s/occupants/%d", location, snep.ID) + + docs.Given("Given another user, who is not an admin") + token := tstValidUserToken(t, 101) + + docs.When("When they try to remove the other attendee from the room") + response := tstPerformDelete(occupantLoc, token) + + docs.Then("Then the request is denied") + tstRequireErrorResponse(t, response, http.StatusForbidden, "auth.forbidden", "you are not authorized for this operation - the attempt has been logged") + + docs.Then("And the room is unchanged") + tstRoomState(t, location, snep) +} + +func TestRoomsRemoveOccupant_UserDenySelfRemove(t *testing.T) { + tstSetup(tstDefaultConfigFileRoomGroups) + defer tstShutdown() + + docs.Given("Given a room with an occupied bed") + location := setupExistingRoom(t, "31415", squirrel) + occupantLoc := fmt.Sprintf("%s/occupants/%d", location, squirrel.ID) + + docs.Given("Given the attendee occupying the bed") + token := tstValidUserToken(t, 101) + + docs.When("When they try to remove themselves from the room") + response := tstPerformDelete(occupantLoc, token) + + docs.Then("Then the request is denied") + tstRequireErrorResponse(t, response, http.StatusForbidden, "auth.forbidden", "you are not authorized for this operation - the attempt has been logged") + + docs.Then("And the room is unchanged") + tstRoomState(t, location, squirrel) +} + +func TestRoomsRemoveOccupant_AdminSuccess(t *testing.T) { + tstSetup(tstDefaultConfigFileRoomGroups) + defer tstShutdown() + + docs.Given("Given a room with occupied beds") + location := setupExistingRoom(t, "31415", squirrel, snep) + squirrelLoc := fmt.Sprintf("%s/occupants/%d", location, squirrel.ID) + + docs.Given("Given an admin") + token := tstValidAdminToken(t) + + docs.When("When the admin removes the attendee from the room") + response := tstPerformDelete(squirrelLoc, token) + + docs.Then("Then the request is successful") + require.Equal(t, http.StatusNoContent, response.status) + + docs.Then("And the attendee has been removed from the room") + tstRoomState(t, location, snep) +} + +func TestRoomsRemoveOccupant_ApiTokenSuccess(t *testing.T) { + tstSetup(tstDefaultConfigFileRoomGroups) + defer tstShutdown() + + docs.Given("Given a room with occupied beds") + location := setupExistingRoom(t, "31415", squirrel, snep) + snepLoc := fmt.Sprintf("%s/occupants/%d", location, snep.ID) + + docs.Given("Given a downstream service using a valid api token") + token := tstValidApiToken() + + docs.When("When it removes an attendee from the room") + response := tstPerformDelete(snepLoc, token) + + docs.Then("Then the request is successful") + require.Equal(t, http.StatusNoContent, response.status) + + docs.Then("And the attendee has been removed from the room") + tstRoomState(t, location, squirrel) +} + +func TestRoomsRemoveOccupant_NotInRoom(t *testing.T) { + tstSetup(tstDefaultConfigFileRoomGroups) + defer tstShutdown() + + docs.Given("Given a room with occupied beds") + location := setupExistingRoom(t, "31415", squirrel) + + docs.Given("Given an attendee with an active registration who is not in any room") + registerSubject(subject(snep)) + snepLoc := fmt.Sprintf("%s/occupants/%d", location, snep.ID) // not actually in the room + + docs.When("When an admin tries to remove the attendee from the room (which they are not actually in)") + token := tstValidAdminToken(t) + response := tstPerformDelete(snepLoc, token) + + docs.Then("Then the request fails with the expected error") + tstRequireErrorResponse(t, response, http.StatusNotFound, "room.occupant.notfound", "this attendee is not in any room") + + docs.Then("And the room is unchanged") + tstRoomState(t, location, squirrel) +} + +func TestRoomsRemoveOccupant_InAnotherRoom(t *testing.T) { + tstSetup(tstDefaultConfigFileRoomGroups) + defer tstShutdown() + + docs.Given("Given a room with occupied beds") + location := setupExistingRoom(t, "31415", squirrel) + + docs.Given("Given an attendee with an active registration who is in another room") + _ = setupExistingRoom(t, "27182", snep) + + docs.When("When an admin tries to remove the attendee from the room (which they are not actually in)") + token := tstValidAdminToken(t) + wrongSnepLoc := fmt.Sprintf("%s/occupants/%d", location, snep.ID) // not actually in this room + response := tstPerformDelete(wrongSnepLoc, token) + + docs.Then("Then the request fails with the expected error") + tstRequireErrorResponse(t, response, http.StatusConflict, "room.occupant.conflict", "this attendee is in a different room") + + docs.Then("And the room is unchanged") + tstRoomState(t, location, squirrel) +} + +func TestRoomsRemoveOccupant_RoomNotFound(t *testing.T) { + tstSetup(tstDefaultConfigFileRoomGroups) + defer tstShutdown() + + docs.Given("Given an admin") + token := tstValidAdminToken(t) + + docs.When("When they try to remove an attendee from a room, but specify a room that does not exist") + response := tstPerformDelete("/api/rest/v1/rooms/7a8d1116-d656-44eb-89dd-51eefef8a83b/occupants/84", token) + + docs.Then("Then the request fails with the appropriate error message") + tstRequireErrorResponse(t, response, http.StatusNotFound, "room.id.notfound", "room does not exist") +} + +func TestRoomsRemoveOccupant_AttendeeNotFound(t *testing.T) { + tstSetup(tstDefaultConfigFileRoomGroups) + defer tstShutdown() + + docs.Given("Given a room with occupied beds") + location := setupExistingRoom(t, "31415", snep) + + docs.Given("Given an admin") + token := tstValidAdminToken(t) + + docs.When("When they try to remove an attendee to the room, but specify a badge number that does not exist") + response := tstPerformDelete(location+"/occupants/4711", token) + + docs.Then("Then the request fails with the appropriate error message") + tstRequireErrorResponse(t, response, http.StatusNotFound, "attendee.notfound", "no such attendee") +} + +func TestRoomsRemoveOccupant_CancelledRemoveSuccess(t *testing.T) { + tstSetup(tstDefaultConfigFileRoomGroups) + defer tstShutdown() + + docs.Given("Given a room with a bed occupied by a cancelled attendee") + location := setupExistingRoom(t, "31415", snep) + snepLoc := fmt.Sprintf("%s/occupants/%d", location, snep.ID) + // now cancel snep - has to be done after adding to room + attMock.SetupRegistered("202", 43, attendeeservice.StatusCancelled, "Snep", "snep@example.com") + + docs.Given("Given an admin") + token := tstValidAdminToken(t) + + docs.When("When the admin removes the attendee from the room") + response := tstPerformDelete(snepLoc, token) + + docs.Then("Then the request is successful, even though their registration is not in attending status") + require.Equal(t, http.StatusNoContent, response.status) + + docs.Then("And the attendee has been removed from the room") + tstRoomState(t, location) +} + +func TestRoomsRemoveOccupant_InvalidRoomID(t *testing.T) { + tstSetup(tstDefaultConfigFileRoomGroups) + defer tstShutdown() + + docs.Given("Given an admin") + token := tstValidAdminToken(t) + + docs.When("When they attempt to remove an attendee from a room, but specify an invalid room id") + response := tstPerformDelete("/api/rest/v1/rooms/kittycats/occupants/84", token) + + docs.Then("Then the request fails with the expected error") + tstRequireErrorResponse(t, response, http.StatusBadRequest, "room.id.invalid", "you must specify a valid uuid") +} + +func TestRoomsRemoveOccupant_BadgeNumberInvalid(t *testing.T) { + tstSetup(tstDefaultConfigFileRoomGroups) + defer tstShutdown() + + docs.Given("Given a room") + location := setupExistingRoom(t, "31415", snep) + + docs.Given("Given an admin") + token := tstValidAdminToken(t) + + docs.When("When they attempt to remove an occupant from the room, but supply an invalid badge number") + response := tstPerformDelete(location+"/occupants/floof", token) + + docs.Then("Then the request fails with the appropriate error message") + tstRequireErrorResponse(t, response, http.StatusBadRequest, "request.parse.failed", "invalid badge number - must be positive integer") +} + +func TestRoomsRemoveOccupant_BadgeNumberNotPositive(t *testing.T) { + tstSetup(tstDefaultConfigFileRoomGroups) + defer tstShutdown() + + docs.Given("Given a room with free beds") + location := setupExistingRoom(t, "31415", snep) + + docs.Given("Given an admin") + token := tstValidAdminToken(t) + + docs.When("When they attempt to remove an occupant from the room, but supply a zero badge number") + response := tstPerformDelete(location+"/occupants/0", token) + + docs.Then("Then the request fails with the appropriate error message") + tstRequireErrorResponse(t, response, http.StatusBadRequest, "room.data.invalid", "invalid badge number - must be positive integer") +} + +// TODO technical errors (downstream failures etc.)