diff --git a/restapi/api/middleware.go b/restapi/api/middleware.go index 1543ef2f..f842d618 100644 --- a/restapi/api/middleware.go +++ b/restapi/api/middleware.go @@ -1,11 +1,12 @@ package api import ( - "github.com/danielpaulus/go-ios/ios" - "github.com/gin-gonic/gin" "net/http" "strings" "sync" + + "github.com/danielpaulus/go-ios/ios" + "github.com/gin-gonic/gin" ) // DeviceMiddleware makes sure a udid was specified and that a device with that UDID @@ -68,3 +69,21 @@ func StreamingHeaderMiddleware() gin.HandlerFunc { c.Next() } } + +func ReserveDevicesMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + udid := c.Param("udid") + reservationID := c.Request.Header.Get("X-GO-IOS-RESERVE") + if reservationID == "" { + c.AbortWithStatusJSON(http.StatusBadRequest, GenericResponse{Error: "Device reservation token empty or missing. This operation requires you to reserve the device using the /reservations endpoint and then pass the reservation token with the X-GO-IOS-RESERVE header"}) + return + } + + err := checkDeviceReserved(udid, reservationID) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, GenericResponse{Error: err.Error()}) + return + } + c.Next() + } +} diff --git a/restapi/api/reservation/reserve_endpoints_test.go b/restapi/api/reservation/reserve_endpoints_test.go deleted file mode 100644 index 03e4a02e..00000000 --- a/restapi/api/reservation/reserve_endpoints_test.go +++ /dev/null @@ -1,208 +0,0 @@ -package reservation - -import ( - "encoding/json" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/danielpaulus/go-ios/ios" - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "github.com/stretchr/testify/require" -) - -var randomDeviceUDID string -var r *gin.Engine - -func setupRouter() *gin.Engine { - randomDeviceUDID = uuid.New().String() - - r := gin.Default() - r.Use(fakeDeviceMiddleware()) - r.POST("/reserve/:udid", ReserveDevice) - r.DELETE("/reserve/:udid", ReleaseDevice) - r.GET("/reserved-devices", GetReservedDevices) - - reservedDevicesMap = make(map[string]*reservedDevice) - return r -} - -func fakeDeviceMiddleware() gin.HandlerFunc { - return func(context *gin.Context) { - context.Set("go_ios_device", ios.DeviceEntry{Properties: ios.DeviceProperties{SerialNumber: randomDeviceUDID}}) - } -} - -// TESTS -func TestDeviceReservation(t *testing.T) { - r = setupRouter() - responseRecorder := httptest.NewRecorder() - - // Reserve the device - reserveRequest := postReservation(t, responseRecorder) - require.Equal(t, http.StatusOK, responseRecorder.Code, "POST to %v was unsuccessful", reserveRequest.URL) - validateSuccessfulReservation(t, responseRecorder) -} - -func TestDeviceReservationAlreadyReserved(t *testing.T) { - r = setupRouter() - responseRecorder := httptest.NewRecorder() - - // Reserve the device - reserveRequest := postReservation(t, responseRecorder) - require.Equal(t, http.StatusOK, responseRecorder.Code, "Initial POST to %v was unsuccessful", reserveRequest.URL) - validateSuccessfulReservation(t, responseRecorder) - - // Try to reserve the already reserved device - reserveRequest = postReservation(t, responseRecorder) - require.Equal(t, http.StatusOK, responseRecorder.Code, "Second POST to %v was unsuccessful", reserveRequest.URL) - validateDeviceAlreadyReserved(t, responseRecorder) -} - -func TestReleasingDevice(t *testing.T) { - r := setupRouter() - responseRecorder := httptest.NewRecorder() - - // Reserve the device - reserveRequest := postReservation(t, responseRecorder) - require.Equal(t, http.StatusOK, responseRecorder.Code, "POST to %v was unsuccessful", reserveRequest.URL) - reserveID := validateSuccessfulReservation(t, responseRecorder) - - // Validate device is in /reserved-devices list - getDevicesRequest := getReservedDevices(t, responseRecorder) - require.Equal(t, http.StatusOK, responseRecorder.Code, "GET %v was unsuccessful", getDevicesRequest.URL) - - var devicesResponse []reservedDevice - responseData, _ := ioutil.ReadAll(responseRecorder.Body) - err := json.Unmarshal(responseData, &devicesResponse) - if err != nil { - t.Error(err) - t.FailNow() - } - - var reservationid_exists = false - for _, device := range devicesResponse { - if device.ReservationID == reserveID { - reservationid_exists = true - require.Equal(t, device.UDID, randomDeviceUDID, "Device UDID does not correspond to the ReservationID, expected UDID=%v, got=%v", randomDeviceUDID, device.UDID) - require.NotEmpty(t, device.LastUsedTimestamp, "`lastUsed` is empty but it shouldn't be") - } - } - require.True(t, reservationid_exists, "Could not find device with `reservation_id`=%v in GET /reserved-devices response", reserveID) - - // Release the reserved device - releaseDeviceRequest := deleteReservation(t, responseRecorder) - require.Equal(t, http.StatusOK, responseRecorder.Code, "DELETE %v was unsuccessful", releaseDeviceRequest.URL) - validateDeviceReleased(t, responseRecorder) - - // Validate no reserved devices present in /reserved-devices response - r.ServeHTTP(responseRecorder, getDevicesRequest) - require.Equal(t, http.StatusOK, responseRecorder.Code, "GET %v was unsuccessful", getDevicesRequest.URL) - - var noReservedDevicesResponse []reservedDevice - responseData, _ = ioutil.ReadAll(responseRecorder.Body) - err = json.Unmarshal(responseData, &noReservedDevicesResponse) - if err != nil { - t.Error(err) - t.FailNow() - } - - require.Equal(t, noReservedDevicesResponse, []reservedDevice{}) -} - -func TestValidateDeviceNotReserved(t *testing.T) { - r = setupRouter() - responseRecorder := httptest.NewRecorder() - - // Validate device not reserved response - releaseDeviceRequest := deleteReservation(t, responseRecorder) - require.Equal(t, http.StatusNotFound, responseRecorder.Code, "DELETE %v was unsuccessful", releaseDeviceRequest.URL) - validateNotReserved(t, responseRecorder) -} - -// HELPER FUNCTIONS -func postReservation(t *testing.T, responseRecorder *httptest.ResponseRecorder) *http.Request { - reserveDevice, err := http.NewRequest("POST", "/reserve/"+randomDeviceUDID, nil) - if err != nil { - t.Error(err) - t.FailNow() - } - r.ServeHTTP(responseRecorder, reserveDevice) - require.Equal(t, http.StatusOK, responseRecorder.Code, "POST to %v was unsuccessful", reserveDevice.URL) - - return reserveDevice -} - -func deleteReservation(t *testing.T, responseRecorder *httptest.ResponseRecorder) *http.Request { - releaseDeviceRequest, err := http.NewRequest("DELETE", "/reserve/"+randomDeviceUDID, nil) - if err != nil { - t.Error(err) - t.FailNow() - } - r.ServeHTTP(responseRecorder, releaseDeviceRequest) - - return releaseDeviceRequest -} - -func getReservedDevices(t *testing.T, responseRecorder *httptest.ResponseRecorder) *http.Request { - getDevicesRequest, err := http.NewRequest("GET", "/reserved-devices", nil) - if err != nil { - t.Error(err) - t.FailNow() - } - r.ServeHTTP(responseRecorder, getDevicesRequest) - - return getDevicesRequest -} - -func validateSuccessfulReservation(t *testing.T, responseRecorder *httptest.ResponseRecorder) string { - var reservationIDResponse reservedDevice - responseData, _ := ioutil.ReadAll(responseRecorder.Body) - err := json.Unmarshal(responseData, &reservationIDResponse) - if err != nil { - t.Error(err) - t.FailNow() - } - - require.NotEmpty(t, reservationIDResponse.ReservationID, "Device was not successfully reserved") - - return reservationIDResponse.ReservationID -} - -func validateDeviceAlreadyReserved(t *testing.T, responseRecorder *httptest.ResponseRecorder) { - var alreadyReservedResponse reservedDevice - responseData, _ := ioutil.ReadAll(responseRecorder.Body) - err := json.Unmarshal(responseData, &alreadyReservedResponse) - if err != nil { - t.Error(err) - t.FailNow() - } - - require.Equal(t, "Already reserved", alreadyReservedResponse.Message) -} - -func validateNotReserved(t *testing.T, responseRecorder *httptest.ResponseRecorder) { - var notReservedResponse reservedDevice - responseData, _ := ioutil.ReadAll(responseRecorder.Body) - err := json.Unmarshal(responseData, ¬ReservedResponse) - if err != nil { - t.Error(err) - t.FailNow() - } - - require.Equal(t, "Not reserved", notReservedResponse.Message) -} - -func validateDeviceReleased(t *testing.T, responseRecorder *httptest.ResponseRecorder) { - var deviceReleasedResponse reservedDevice - responseData, _ := ioutil.ReadAll(responseRecorder.Body) - err := json.Unmarshal(responseData, &deviceReleasedResponse) - if err != nil { - t.Error(err) - t.FailNow() - } - - require.Equal(t, "Successfully released", deviceReleasedResponse.Message) -} diff --git a/restapi/api/reservation/reserve_endpoints.go b/restapi/api/reserve_endpoints.go similarity index 64% rename from restapi/api/reservation/reserve_endpoints.go rename to restapi/api/reserve_endpoints.go index 67384c1a..eb4dfdda 100644 --- a/restapi/api/reservation/reserve_endpoints.go +++ b/restapi/api/reserve_endpoints.go @@ -1,6 +1,7 @@ -package reservation +package api import ( + "errors" "net/http" "sync" "time" @@ -11,7 +12,8 @@ import ( var reservedDevicesMap = make(map[string]*reservedDevice) var reserveMutex sync.Mutex -var ReservedDevicesTimeout time.Duration = 5 +var reservedDevicesTimeout time.Duration = 5 +var reserveAdminUUID = "go-admin" type reservedDevice struct { Message string `json:"message,omitempty"` @@ -20,33 +22,15 @@ type reservedDevice struct { LastUsedTimestamp int64 `json:"lastUsed,omitempty"` } -func CleanReservationsCRON() { - defer reserveMutex.Unlock() - - // Every minute loop through the map of reserved devices and check if a reserved device last used timestamp was more than 5 minutes(300000 ms) ago - // If any, remove them from the map - for range time.Tick(time.Second * 60) { - reserveMutex.Lock() - for udid, reservedDevice := range reservedDevicesMap { - currentTimestamp := time.Now().UnixMilli() - diff := currentTimestamp - reservedDevice.LastUsedTimestamp - - if diff > (time.Minute * ReservedDevicesTimeout).Milliseconds() { - delete(reservedDevicesMap, udid) - } - } - reserveMutex.Unlock() - } -} - // Reserve device access // List godoc // @Summary Reserve a device // @Description Reserve a device by provided UDID -// @Tags reserve +// @Tags reservations +// @Param udid path string true "device udid" // @Produce json // @Success 200 {object} reservedDevice -// @Router /reserve/:udid [post] +// @Router /{udid}/reservations [post] func ReserveDevice(c *gin.Context) { udid := c.Param("udid") reservationID := uuid.New().String() @@ -71,36 +55,37 @@ func ReserveDevice(c *gin.Context) { // List godoc // @Summary Release a device // @Description Release a device by provided UDID -// @Tags reserve +// @Tags reservations +// @Param reservationID path string true "reservation ID generated when reserving device" // @Produce json // @Success 200 {object} reservedDevice // @Failure 404 {object} reservedDevice -// @Router /reserve/:udid [delete] +// @Router /reservations/{reservationID} [delete] func ReleaseDevice(c *gin.Context) { - udid := c.Param("udid") + reservationID := c.Param("reservationID") reserveMutex.Lock() defer reserveMutex.Unlock() - // Check if there is a reserved device for the respective UDID - device := reservedDevicesMap[udid] - if device == nil { - c.IndentedJSON(http.StatusNotFound, reservedDevice{Message: "Not reserved"}) - return + for udid, device := range reservedDevicesMap { + if device.ReservationID == reservationID { + delete(reservedDevicesMap, udid) + c.IndentedJSON(http.StatusOK, reservedDevice{Message: "Successfully released"}) + return + } } - delete(reservedDevicesMap, udid) - c.IndentedJSON(http.StatusOK, reservedDevice{Message: "Successfully released"}) + c.IndentedJSON(http.StatusNotFound, reservedDevice{Message: "Not reserved or wrong reservationID"}) } // Get all reserved devices // List godoc // @Summary Get a list of reserved devices // @Description Get a list of reserved devices with UDID, ReservationID and last used timestamp -// @Tags reserve +// @Tags reservations // @Produce json // @Success 200 {object} []reservedDevice -// @Router /reserved-devices [get] +// @Router /reservations [get] func GetReservedDevices(c *gin.Context) { reserveMutex.Lock() defer reserveMutex.Unlock() @@ -123,3 +108,38 @@ func GetReservedDevices(c *gin.Context) { c.IndentedJSON(http.StatusOK, reserved_devices) } + +func cleanReservationsCRON() { + defer reserveMutex.Unlock() + + // Every minute loop through the map of reserved devices and check if a reserved device last used timestamp was more than X minutes ago + // If any, remove them from the map + for range time.Tick(time.Second * 60) { + reserveMutex.Lock() + for udid, reservedDevice := range reservedDevicesMap { + currentTimestamp := time.Now().UnixMilli() + diff := currentTimestamp - reservedDevice.LastUsedTimestamp + + if diff > (time.Minute * reservedDevicesTimeout).Milliseconds() { + delete(reservedDevicesMap, udid) + } + } + reserveMutex.Unlock() + } +} + +func checkDeviceReserved(deviceUDID string, reservationID string) error { + reserveMutex.Lock() + defer reserveMutex.Unlock() + + reservedDevice, exists := reservedDevicesMap[deviceUDID] + + if exists { + if reservedDevice.ReservationID == reservationID || reserveAdminUUID == reservationID { + reservedDevice.LastUsedTimestamp = time.Now().UnixMilli() + return nil + } + return errors.New("Device is already reserved with another reservationID") + } + return errors.New("You need to reserve the device before using it") +} diff --git a/restapi/api/reserve_endpoints_test.go b/restapi/api/reserve_endpoints_test.go new file mode 100644 index 00000000..094d5cb8 --- /dev/null +++ b/restapi/api/reserve_endpoints_test.go @@ -0,0 +1,363 @@ +package api + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/danielpaulus/go-ios/ios" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +var randomDeviceUDID string +var r *gin.Engine + +func setupRouter() *gin.Engine { + randomDeviceUDID = uuid.New().String() + + r := gin.Default() + r.Use(fakeDeviceMiddleware()) + r.POST("/:udid/reservations", ReserveDevice) + r.DELETE("/reservations/:reservationID", ReleaseDevice) + r.GET("/reservations", GetReservedDevices) + + reservedDevicesMap = make(map[string]*reservedDevice) + return r +} + +func fakeDeviceMiddleware() gin.HandlerFunc { + return func(context *gin.Context) { + context.Set("go_ios_device", ios.DeviceEntry{Properties: ios.DeviceProperties{SerialNumber: randomDeviceUDID}}) + } +} + +// TESTS +func TestDeviceReservation(t *testing.T) { + r = setupRouter() + responseRecorder := httptest.NewRecorder() + + // Reserve the device + postReservation(t, responseRecorder) + validateSuccessfulReservation(t, responseRecorder) +} + +func TestDeviceReservationAlreadyReserved(t *testing.T) { + r = setupRouter() + responseRecorder := httptest.NewRecorder() + + // Reserve the device + postReservation(t, responseRecorder) + validateSuccessfulReservation(t, responseRecorder) + + // Try to reserve the already reserved device + responseRecorder = httptest.NewRecorder() + postReservation(t, responseRecorder) + validateDeviceAlreadyReserved(t, responseRecorder) +} + +func TestReleasingDevice(t *testing.T) { + r := setupRouter() + responseRecorder := httptest.NewRecorder() + + // Reserve the device + postReservation(t, responseRecorder) + reservationID := validateSuccessfulReservation(t, responseRecorder) + + // Validate device is in /reservations list + responseRecorder = httptest.NewRecorder() + getDevicesRequest := getReservedDevices(t, responseRecorder) + require.Equal(t, http.StatusOK, responseRecorder.Code, "GET %v was unsuccessful", getDevicesRequest.URL) + + var devicesResponse []reservedDevice + responseData, _ := io.ReadAll(responseRecorder.Body) + err := json.Unmarshal(responseData, &devicesResponse) + if err != nil { + t.Error(err) + t.FailNow() + } + + var reservationid_exists = false + for _, device := range devicesResponse { + if device.ReservationID == reservationID { + reservationid_exists = true + require.Equal(t, device.UDID, randomDeviceUDID, "Device UDID does not correspond to the ReservationID, expected UDID=%v, got=%v", randomDeviceUDID, device.UDID) + require.NotEmpty(t, device.LastUsedTimestamp, "`lastUsed` is empty but it shouldn't be") + } + } + require.True(t, reservationid_exists, "Could not find device with `reservationID`=%v in GET /reservations response", reservationID) + + // Release the reserved device + responseRecorder = httptest.NewRecorder() + releaseDeviceRequest := deleteReservation(t, responseRecorder, reservationID) + require.Equal(t, http.StatusOK, responseRecorder.Code, "DELETE %v was unsuccessful", releaseDeviceRequest.URL) + validateDeviceReleased(t, responseRecorder) + + // Validate no reserved devices present in /reservations response + r.ServeHTTP(responseRecorder, getDevicesRequest) + require.Equal(t, http.StatusOK, responseRecorder.Code, "GET %v was unsuccessful", getDevicesRequest.URL) + + var noReservedDevicesResponse []reservedDevice + responseData, _ = io.ReadAll(responseRecorder.Body) + err = json.Unmarshal(responseData, &noReservedDevicesResponse) + if err != nil { + t.Error(err) + t.FailNow() + } + + require.Equal(t, noReservedDevicesResponse, []reservedDevice{}) +} + +func TestValidateDeviceNotReserved(t *testing.T) { + r = setupRouter() + responseRecorder := httptest.NewRecorder() + + // Validate device not reserved response + releaseDeviceRequest := deleteReservation(t, responseRecorder, "test") + require.Equal(t, http.StatusNotFound, responseRecorder.Code, "DELETE %v was unsuccessful", releaseDeviceRequest.URL) + validateNotReserved(t, responseRecorder) +} + +func TestValidateMiddlewareHeaderMissing(t *testing.T) { + r = setupRouter() + r.Use(ReserveDevicesMiddleware()) + r.POST("/:udid/launch", LaunchApp) + + responseRecorder := httptest.NewRecorder() + + launchAppRequest, err := http.NewRequest("POST", "/"+randomDeviceUDID+"/launch?bundleID=com.apple.Preferences", nil) + if err != nil { + t.Error(err) + t.FailNow() + } + r.ServeHTTP(responseRecorder, launchAppRequest) + require.Equal(t, http.StatusBadRequest, responseRecorder.Code, "Code should be BadRequest if X-GO-IOS-RESERVE header is missing") + + var response GenericResponse + responseData, _ := io.ReadAll(responseRecorder.Body) + err = json.Unmarshal(responseData, &response) + if err != nil { + t.Error(err) + t.FailNow() + } + + require.NotEmpty(t, response.Error, "There is no error message returned when X-GO-IOS-RESERVE header is missing") +} + +func TestValidateMiddlewareHeaderEmpty(t *testing.T) { + r = setupRouter() + r.Use(ReserveDevicesMiddleware()) + r.POST("/:udid/launch", LaunchApp) + + responseRecorder := httptest.NewRecorder() + + launchAppRequest, err := http.NewRequest("POST", "/"+randomDeviceUDID+"/launch?bundleID=com.apple.Preferences", nil) + if err != nil { + t.Error(err) + t.FailNow() + } + launchAppRequest.Header.Add("X-GO-IOS-RESERVE", "") + r.ServeHTTP(responseRecorder, launchAppRequest) + require.Equal(t, http.StatusBadRequest, responseRecorder.Code, "Code should be BadRequest if X-GO-IOS-RESERVE header is empty") + + var response GenericResponse + responseData, _ := io.ReadAll(responseRecorder.Body) + err = json.Unmarshal(responseData, &response) + if err != nil { + t.Error(err) + t.FailNow() + } + + require.NotEmpty(t, response.Error, "There is no error message returned when X-GO-IOS-RESERVE header is missing") +} + +func TestValidateMiddlewareHeaderDeviceNotReserved(t *testing.T) { + r = setupRouter() + r.Use(ReserveDevicesMiddleware()) + r.POST("/:udid/launch", LaunchApp) + + responseRecorder := httptest.NewRecorder() + + launchAppRequest, err := http.NewRequest("POST", "/"+randomDeviceUDID+"/launch?bundleID=com.apple.Preferences", nil) + if err != nil { + t.Error(err) + t.FailNow() + } + launchAppRequest.Header.Add("X-GO-IOS-RESERVE", "go-admin") + r.ServeHTTP(responseRecorder, launchAppRequest) + require.Equal(t, http.StatusBadRequest, responseRecorder.Code, "Code should be BadRequest if X-GO-IOS-RESERVE header is empty") + + var response GenericResponse + responseData, _ := io.ReadAll(responseRecorder.Body) + err = json.Unmarshal(responseData, &response) + if err != nil { + t.Error(err) + t.FailNow() + } + + require.NotEmpty(t, response.Error, "There is no error message returned when X-GO-IOS-RESERVE header is missing") +} + +func TestValidateMiddlewareDeviceReservedWrongUUID(t *testing.T) { + r = setupRouter() + r.Use(ReserveDevicesMiddleware()) + r.POST("/:udid/launch", LaunchApp) + + responseRecorder := httptest.NewRecorder() + + postReservation(t, responseRecorder) + + responseRecorder = httptest.NewRecorder() + launchAppRequest, err := http.NewRequest("POST", "/"+randomDeviceUDID+"/launch?bundleID=com.apple.Preferences", nil) + if err != nil { + t.Error(err) + t.FailNow() + } + launchAppRequest.Header.Set("X-GO-IOS-RESERVE", "bad-uuid") + r.ServeHTTP(responseRecorder, launchAppRequest) + require.Equal(t, http.StatusBadRequest, responseRecorder.Code, "Code should be BadRequest if X-GO-IOS-RESERVE header is empty") + + var response GenericResponse + responseData, _ := io.ReadAll(responseRecorder.Body) + err = json.Unmarshal(responseData, &response) + if err != nil { + t.Error(err) + t.FailNow() + } + + require.NotEmpty(t, response.Error, "There is no error message returned when X-GO-IOS-RESERVE header is missing") +} + +func TestValidateMiddlewareDeviceReservedValidUUID(t *testing.T) { + r = setupRouter() + r.Use(ReserveDevicesMiddleware()) + r.POST("/:udid/launch", LaunchApp) + + responseRecorder := httptest.NewRecorder() + + postReservation(t, responseRecorder) + reservationID := validateSuccessfulReservation(t, responseRecorder) + + responseRecorder = httptest.NewRecorder() + launchAppRequest, err := http.NewRequest("POST", "/"+randomDeviceUDID+"/launch?bundleID=com.apple.Preferences", nil) + if err != nil { + t.Error(err) + t.FailNow() + } + launchAppRequest.Header.Set("X-GO-IOS-RESERVE", reservationID) + r.ServeHTTP(responseRecorder, launchAppRequest) + // Launching app does not really work with a mocked device + // We check that status is not 400 because in the current scenario 400 is only returned when there is a problem with the reservation header + require.NotEqual(t, http.StatusBadRequest, responseRecorder.Code) +} + +func TestValidateMiddlewareDeviceReservedAdminUUID(t *testing.T) { + r = setupRouter() + r.Use(ReserveDevicesMiddleware()) + r.POST("/:udid/launch", LaunchApp) + + responseRecorder := httptest.NewRecorder() + + postReservation(t, responseRecorder) + validateSuccessfulReservation(t, responseRecorder) + + responseRecorder = httptest.NewRecorder() + launchAppRequest, err := http.NewRequest("POST", "/"+randomDeviceUDID+"/launch?bundleID=com.apple.Preferences", nil) + if err != nil { + t.Error(err) + t.FailNow() + } + launchAppRequest.Header.Set("X-GO-IOS-RESERVE", reserveAdminUUID) + r.ServeHTTP(responseRecorder, launchAppRequest) + // Launching app does not really work with a mocked device + // We check that status is not 400 because in the current scenario 400 is only returned when there is a problem with the reservation header + require.NotEqual(t, http.StatusBadRequest, responseRecorder.Code) +} + +// HELPER FUNCTIONS +func postReservation(t *testing.T, responseRecorder *httptest.ResponseRecorder) *http.Request { + reserveDevice, err := http.NewRequest("POST", "/"+randomDeviceUDID+"/reservations", nil) + if err != nil { + t.Error(err) + t.FailNow() + } + r.ServeHTTP(responseRecorder, reserveDevice) + require.Equal(t, http.StatusOK, responseRecorder.Code, "POST to %v was unsuccessful", reserveDevice.URL) + + return reserveDevice +} + +func deleteReservation(t *testing.T, responseRecorder *httptest.ResponseRecorder, reservationID string) *http.Request { + releaseDeviceRequest, err := http.NewRequest("DELETE", "/reservations/"+reservationID, nil) + if err != nil { + t.Error(err) + t.FailNow() + } + r.ServeHTTP(responseRecorder, releaseDeviceRequest) + + return releaseDeviceRequest +} + +func getReservedDevices(t *testing.T, responseRecorder *httptest.ResponseRecorder) *http.Request { + getDevicesRequest, err := http.NewRequest("GET", "/reservations", nil) + if err != nil { + t.Error(err) + t.FailNow() + } + r.ServeHTTP(responseRecorder, getDevicesRequest) + + return getDevicesRequest +} + +func validateSuccessfulReservation(t *testing.T, responseRecorder *httptest.ResponseRecorder) string { + var reservationIDResponse reservedDevice + responseData, _ := io.ReadAll(responseRecorder.Body) + err := json.Unmarshal(responseData, &reservationIDResponse) + if err != nil { + t.Error(err) + t.FailNow() + } + + require.NotEmpty(t, reservationIDResponse.ReservationID, "Device was not successfully reserved") + + return reservationIDResponse.ReservationID +} + +func validateDeviceAlreadyReserved(t *testing.T, responseRecorder *httptest.ResponseRecorder) { + var alreadyReservedResponse reservedDevice + responseData, _ := io.ReadAll(responseRecorder.Body) + err := json.Unmarshal(responseData, &alreadyReservedResponse) + if err != nil { + t.Error(err) + t.FailNow() + } + + require.Equal(t, "Already reserved", alreadyReservedResponse.Message) +} + +func validateNotReserved(t *testing.T, responseRecorder *httptest.ResponseRecorder) { + var notReservedResponse reservedDevice + responseData, _ := io.ReadAll(responseRecorder.Body) + err := json.Unmarshal(responseData, ¬ReservedResponse) + if err != nil { + t.Error(err) + t.FailNow() + } + + require.Equal(t, "Not reserved or wrong reservationID", notReservedResponse.Message) +} + +func validateDeviceReleased(t *testing.T, responseRecorder *httptest.ResponseRecorder) { + var deviceReleasedResponse reservedDevice + responseData, _ := io.ReadAll(responseRecorder.Body) + err := json.Unmarshal(responseData, &deviceReleasedResponse) + if err != nil { + t.Error(err) + t.FailNow() + } + + require.Equal(t, "Successfully released", deviceReleasedResponse.Message) +} diff --git a/restapi/api/routes.go b/restapi/api/routes.go index 232b8922..85a9efef 100644 --- a/restapi/api/routes.go +++ b/restapi/api/routes.go @@ -1,12 +1,14 @@ package api import ( - "github.com/danielpaulus/go-ios/restapi/api/reservation" "github.com/gin-gonic/gin" ) func registerRoutes(router *gin.RouterGroup) { router.GET("/list", List) + router.GET("/reservations", GetReservedDevices) + router.DELETE("/reservations/:reservationID", ReleaseDevice) + device := router.Group("/device/:udid") device.Use(DeviceMiddleware()) device.GET("/info", Info) @@ -17,22 +19,18 @@ func registerRoutes(router *gin.RouterGroup) { device.PUT("/enable-condition", EnableDeviceCondition) device.POST("/disable-condition", DisableDeviceCondition) - router.GET("/reserved-devices", reservation.GetReservedDevices) - reservations := router.Group("/reserve/:udid") - reservations.Use(DeviceMiddleware()) - reservations.POST("/", reservation.ReserveDevice) - reservations.DELETE("/", reservation.ReleaseDevice) + device.POST("/reservations", ReserveDevice) initAppRoutes(device) initStreamingResponseRoutes(device, router) - go reservation.CleanReservationsCRON() + go cleanReservationsCRON() } func initAppRoutes(group *gin.RouterGroup) { router := group.Group("/apps") + router.Use(LimitNumClientsUDID()) router.GET("/", ListApps) router.POST("/launch", LaunchApp) router.POST("/kill", KillApp) - router.Use(LimitNumClientsUDID()) } func initStreamingResponseRoutes(device *gin.RouterGroup, router *gin.RouterGroup) {