-
-
Notifications
You must be signed in to change notification settings - Fork 197
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add lock endpoints to reserve devices (#187)
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
Showing
5 changed files
with
356 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, ¬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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters