Skip to content

Commit

Permalink
Add lock endpoints to reserve devices (#187)
Browse files Browse the repository at this point in the history
This is the first batch of code for #182

Added POST /reserve/:udid endpoint to reserve a device by provided UDID
Added DELETE /reserve/:udid endpoint to release a reserved device by provided UDID
Added GET /reserved-devices endpoint to get a list of the currently reserved devices
Added a goroutine that will check reserved devices every minute and remove devices that were not used for more than 5 minutes
Added API tests for the new package, not really sure if that's what was meant by 'integration tests'
  • Loading branch information
shamanec authored Oct 26, 2022
1 parent c985206 commit 3507f94
Show file tree
Hide file tree
Showing 5 changed files with 356 additions and 5 deletions.
125 changes: 125 additions & 0 deletions restapi/api/reservation/reserve_endpoints.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package reservation

import (
"net/http"
"sync"
"time"

"github.com/gin-gonic/gin"
"github.com/google/uuid"
)

var reservedDevicesMap = make(map[string]*reservedDevice)
var reserveMutex sync.Mutex
var ReservedDevicesTimeout time.Duration = 5

type reservedDevice struct {
Message string `json:"message,omitempty"`
UDID string `json:"udid,omitempty"`
ReservationID string `json:"reservationID,omitempty"`
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
// @Produce json
// @Success 200 {object} reservedDevice
// @Router /reserve/:udid [post]
func ReserveDevice(c *gin.Context) {
udid := c.Param("udid")
reservationID := uuid.New().String()

reserveMutex.Lock()
defer reserveMutex.Unlock()

// Check if there is a reserved device for the respective UDID
_, exists := reservedDevicesMap[udid]
if exists {
c.IndentedJSON(http.StatusOK, reservedDevice{Message: "Already reserved"})
return
}

newReservedDevice := reservedDevice{ReservationID: reservationID, LastUsedTimestamp: time.Now().UnixMilli()}
reservedDevicesMap[udid] = &newReservedDevice

c.IndentedJSON(http.StatusOK, reservedDevice{ReservationID: reservationID})
}

// Release device access
// List godoc
// @Summary Release a device
// @Description Release a device by provided UDID
// @Tags reserve
// @Produce json
// @Success 200 {object} reservedDevice
// @Failure 404 {object} reservedDevice
// @Router /reserve/:udid [delete]
func ReleaseDevice(c *gin.Context) {
udid := c.Param("udid")

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
}

delete(reservedDevicesMap, udid)
c.IndentedJSON(http.StatusOK, reservedDevice{Message: "Successfully released"})
}

// 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
// @Produce json
// @Success 200 {object} []reservedDevice
// @Router /reserved-devices [get]
func GetReservedDevices(c *gin.Context) {
reserveMutex.Lock()
defer reserveMutex.Unlock()

var reserved_devices = []reservedDevice{}

if len(reservedDevicesMap) == 0 {
c.IndentedJSON(http.StatusOK, reserved_devices)
return
}

// Build the JSON array of currently reserved devices
for udid, device := range reservedDevicesMap {
reserved_devices = append(reserved_devices, reservedDevice{
UDID: udid,
ReservationID: device.ReservationID,
LastUsedTimestamp: device.LastUsedTimestamp,
})
}

c.IndentedJSON(http.StatusOK, reserved_devices)
}
208 changes: 208 additions & 0 deletions restapi/api/reservation/reserve_endpoints_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
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, &notReservedResponse)
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)
}
12 changes: 11 additions & 1 deletion restapi/api/routes.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package api

import "github.com/gin-gonic/gin"
import (
"github.com/danielpaulus/go-ios/restapi/api/reservation"
"github.com/gin-gonic/gin"
)

func registerRoutes(router *gin.RouterGroup) {
router.GET("/list", List)
Expand All @@ -11,8 +14,15 @@ func registerRoutes(router *gin.RouterGroup) {
device.PUT("/setlocation", SetLocation)
device.POST("/resetlocation", ResetLocation)

router.GET("/reserved-devices", reservation.GetReservedDevices)
reservations := router.Group("/reserve/:udid")
reservations.Use(DeviceMiddleware())
reservations.POST("/", reservation.ReserveDevice)
reservations.DELETE("/", reservation.ReleaseDevice)

initAppRoutes(device)
initStreamingResponseRoutes(device, router)
go reservation.CleanReservationsCRON()
}
func initAppRoutes(group *gin.RouterGroup) {
router := group.Group("/app")
Expand Down
9 changes: 5 additions & 4 deletions restapi/api/server.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package api

import (
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/swaggo/files"
"github.com/swaggo/gin-swagger"
"io"
"os"

"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)

func Main() {
Expand Down
7 changes: 7 additions & 0 deletions restapi/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/stretchr/testify v1.8.0
github.com/ugorji/go/codec v1.2.7 // indirect
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect
Expand All @@ -48,4 +49,10 @@ require (
howett.net/plist v1.0.0 // indirect
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace github.com/danielpaulus/go-ios => ../

0 comments on commit 3507f94

Please sign in to comment.