diff --git a/README.md b/README.md index f6e9921..e89bebe 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ To run the server locally * `scripts/deploy.sh $VERSION_NUMBER` - Deploys any changes to kubernetes manifests, builds a new docker image, pushes it to docker hub and finally scales the deployment to pull the newly created image. * `scripts/deploy_kubernetes_config.sh` - Deploys just kubernetes manifest changes (kubernetes secret is excluded from the script). * `scripts/push_docker.sh $VERSION_NUMBER` - Builds and pushes the code to dockerhub with a $VERSION_NUMBER as a tag. + * `go test -v ./...` - Runs all tests ## Deployment diff --git a/http-routes/http-routes.go b/http-routes/http-routes.go index 52f6c5c..1575788 100644 --- a/http-routes/http-routes.go +++ b/http-routes/http-routes.go @@ -1,22 +1,23 @@ package httproutes import ( - "log" "encoding/json" + "log" "net/http" + "github.com/gorilla/websocket" - "github.com/jaskaransarkaria/programming-timer-server/session" "github.com/jaskaransarkaria/programming-timer-server/readers" + "github.com/jaskaransarkaria/programming-timer-server/session" "github.com/jaskaransarkaria/programming-timer-server/utils" ) var upgrader = websocket.Upgrader{ // empty struct means use defaults - ReadBufferSize: 1024, + ReadBufferSize: 1024, WriteBufferSize: 1024, } -func enableCors(w *http.ResponseWriter) {(*w).Header().Set("Access-Control-Allow-Origin", "*")} +func enableCors(w *http.ResponseWriter) { (*w).Header().Set("Access-Control-Allow-Origin", "*") } func wsEndpoint(w http.ResponseWriter, r *http.Request) { // this is for CORS - allow all origin @@ -42,24 +43,53 @@ func updateSessionEndpoint(w http.ResponseWriter, r *http.Request) { session.UpdateTimerChannel <- sessionToUpdate } +func pauseSessionEndpoint(w http.ResponseWriter, r *http.Request) { + var sessionToPause session.PauseRequest + var requestBody = r.Body + log.Println("request body", requestBody) + enableCors(&w) + err := json.NewDecoder(requestBody).Decode(&sessionToPause) + log.Println("pause session endpoint reached", sessionToPause) + if err != nil { + log.Println(err) + } + defer r.Body.Close() + session.PauseTimerChannel <- sessionToPause +} + +func unpauseSessionEndpoint(w http.ResponseWriter, r *http.Request) { + var sessionToUnpause session.UnpauseRequest + var requestBody = r.Body + log.Println("request body", requestBody) + enableCors(&w) + err := json.NewDecoder(requestBody).Decode(&sessionToUnpause) + log.Println("unpause session endpoint reached", sessionToUnpause) + if err != nil { + log.Println(err) + } + defer r.Body.Close() + session.UnpauseTimerChannel <- sessionToUnpause +} + func newSessionEndpoint(w http.ResponseWriter, r *http.Request) { var timerRequest session.StartTimerReq var requestBody = r.Body + log.Println(requestBody) enableCors(&w) err := json.NewDecoder(requestBody).Decode(&timerRequest) if err != nil { log.Println(err) } defer r.Body.Close() - newUser := session.User{ UUID: utils.GenerateRandomID("user") } + newUser := session.User{UUID: utils.GenerateRandomID("user")} newSession := session.CreateNewUserAndSession( timerRequest, - newUser, + newUser, utils.GenerateRandomID, ) resp := session.InitSessionResponse{ - Session: newSession, - User: newUser, + Session: newSession, + User: newUser, } newSessionRes, _ := json.Marshal(resp) w.Write(newSessionRes) @@ -74,22 +104,22 @@ func joinSessionEndpoint(w http.ResponseWriter, r *http.Request) { log.Println(err) } defer r.Body.Close() - var newUser = session.User{ UUID: utils.GenerateRandomID("user") } + var newUser = session.User{UUID: utils.GenerateRandomID("user")} matchedSession, err := session.JoinExistingSession(sessionRequest, newUser) if err != nil { bufferedErr, _ := json.Marshal(err) w.Write(bufferedErr) } resp := session.InitSessionResponse{ - Session: matchedSession, - User: newUser, + Session: matchedSession, + User: newUser, } bufferedExistingSession, _ := json.Marshal(resp) w.Write(bufferedExistingSession) } func SetupRoutes() { - http.HandleFunc("/healthz", func (w http.ResponseWriter, r *http.Request) { + http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) w.Write([]byte("ok")) }) @@ -98,4 +128,8 @@ func SetupRoutes() { http.HandleFunc("/session/join", joinSessionEndpoint) http.HandleFunc("/session/update", updateSessionEndpoint) go readers.UpdateChannelReader() + http.HandleFunc("/session/pause", pauseSessionEndpoint) + go readers.PauseChannelReader() + http.HandleFunc("/session/unpause", unpauseSessionEndpoint) + go readers.UnpauseChannelReader() } diff --git a/main.go b/main.go index f6667d8..c6e4209 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,14 @@ package main import ( - "github.com/jaskaransarkaria/programming-timer-server/http-routes" + "flag" "fmt" - "net/http" "log" - "flag" + "net/http" + + httproutes "github.com/jaskaransarkaria/programming-timer-server/http-routes" ) + // flag allows you to create cli flags and assign a default var addr = flag.String("addr", "0.0.0.0:8080", "http service address") diff --git a/readers/readers.go b/readers/readers.go index 9e4442e..e73e1cc 100644 --- a/readers/readers.go +++ b/readers/readers.go @@ -2,6 +2,7 @@ package readers import ( "log" + "github.com/gorilla/websocket" "github.com/jaskaransarkaria/programming-timer-server/session" ) @@ -18,7 +19,7 @@ func ConnReader(conn *websocket.Conn) { } conn.Close() break - } else { + } else { log.Println(string(p)) addToSessionErr := session.AddUserConnToSession(string(p), conn) if addToSessionErr != nil { @@ -31,7 +32,23 @@ func ConnReader(conn *websocket.Conn) { // UpdateChannelReader handle updates to sessions func UpdateChannelReader() { for { - recievedUpdate := <- session.UpdateTimerChannel + recievedUpdate := <-session.UpdateTimerChannel session.HandleUpdateSession(recievedUpdate) } } + +//PauseChannelReader handles pause requests +func PauseChannelReader() { + for { + pauseRequest := <-session.PauseTimerChannel + session.HandlePauseSession(pauseRequest) + } +} + +//UnpauseChannelReader handles restart requests +func UnpauseChannelReader() { + for { + unpauseRequest := <-session.UnpauseTimerChannel + session.HandleUnpauseSession(unpauseRequest) + } +} diff --git a/session/session.go b/session/session.go index bca59e0..b658c49 100644 --- a/session/session.go +++ b/session/session.go @@ -1,14 +1,15 @@ package session import ( - "log" "errors" + "log" "time" + "github.com/gorilla/websocket" "github.com/jaskaransarkaria/programming-timer-server/utils" ) -// Connector is .. the User's current connection +// Connector is .. the User's current connection type Connector interface { WriteJSON(v interface{}) error ReadMessage() (int, []byte, error) @@ -22,24 +23,34 @@ type User struct { // Session is ... each active session type Session struct { - SessionID string - CurrentDriver User - Duration int64 - StartTime int64 - EndTime int64 + SessionID string + CurrentDriver User + Duration int64 + StartTime int64 + EndTime int64 PreviousDrivers []User - Users []User + Users []User +} + +// PauseSessionResponse ... the time when a user pauses the timer +type PauseSessionResponse struct { + PauseTime int64 +} + +// UnpauseSessionResponse ... the time when a user restarts the timer +type UnpauseSessionResponse struct { + UnpauseTime int64 } // InitSessionResponse is ... on inital connection type InitSessionResponse struct { Session Session - User User + User User } // StartTimerReq ... JSON request from the client type StartTimerReq struct { - Duration int64 `json:"duration"` + Duration int64 `json:"duration"` StartTime int64 `json:"startTime"` } @@ -50,8 +61,20 @@ type ExistingSessionReq struct { // UpdateRequest .. Incoming timer update from client (current driver) type UpdateRequest struct { + SessionID string `json:"sessionId"` + UpdatedDuration int64 `json:"updatedDuration,omitempty"` +} + +// PauseRequest ... incoming pause time and session ID from client +type PauseRequest struct { SessionID string `json:"sessionId"` - UpdatedDuration int64 `json:"updatedDuration,omitempty"` + PauseTime int64 `json:"pauseTime"` +} + +// UnpauseRequest ... incoming pause time and session ID from client +type UnpauseRequest struct { + SessionID string `json:"sessionId"` + UnpauseTime int64 `json:"unpauseTime"` } // Sessions is a collection of all current sessions @@ -60,18 +83,24 @@ var Sessions []Session // UpdateTimerChannel reads updates as they come in via updateSessionEndpoint var UpdateTimerChannel = make(chan UpdateRequest) +// PauseTimerChannel reads pause requests as they come in via pauseSessionEndpoint +var PauseTimerChannel = make(chan PauseRequest) + +// UnpauseTimerChannel reads restart requests as they come in via unpauseSessionEndpoint +var UnpauseTimerChannel = make(chan UnpauseRequest) + // CreateNewUserAndSession creates new users and sessions func CreateNewUserAndSession( newSessionData StartTimerReq, newUser User, generateIDFunc utils.RandomGenerator, - ) Session { +) Session { var newSession = Session{ - SessionID: generateIDFunc("session"), + SessionID: generateIDFunc("session"), CurrentDriver: newUser, - Duration: newSessionData.Duration, - StartTime: newSessionData.StartTime, - EndTime: newSessionData.Duration + newSessionData.StartTime, + Duration: newSessionData.Duration, + StartTime: newSessionData.StartTime, + EndTime: newSessionData.Duration + newSessionData.StartTime, } newSession.addUser(newUser) Sessions = append(Sessions, newSession) @@ -81,11 +110,11 @@ func CreateNewUserAndSession( // AddUserConnToSession adds the ws connection to the relevant session func AddUserConnToSession(uuid string, conn *websocket.Conn) error { sessionIdx, sessionErr := findSession(uuid) - if sessionErr != nil { + if sessionErr != nil { return sessionErr } userIdx, userErr := Sessions[sessionIdx].findUser(uuid) - if userErr != nil { + if userErr != nil { return userErr } Sessions[sessionIdx].Users[userIdx].Conn = conn @@ -109,11 +138,33 @@ func HandleUpdateSession(sessionToUpdate UpdateRequest) { log.Println("updateError", updateErr) return } - Sessions[updatedSessionIdx].broadcastToSessionUsers() + Sessions[updatedSessionIdx].broadcast(Sessions[updatedSessionIdx]) +} + +// HandlePauseSession when the driver pauses the timer +func HandlePauseSession(sessionToPause PauseRequest) { + pauseTime := PauseSessionResponse{PauseTime: sessionToPause.PauseTime} + pausedSessionIdx, pauseErr := getExistingSession(sessionToPause.SessionID) + if pauseErr != nil { + log.Println("pauseError", pauseErr) + return + } + Sessions[pausedSessionIdx].broadcast(pauseTime) +} + +// HandleUnpauseSession when the driver pauses the timer +func HandleUnpauseSession(sessionToUnpause UnpauseRequest) { + unpauseTime := UnpauseSessionResponse{UnpauseTime: sessionToUnpause.UnpauseTime} + unpausedSessionIdx, unpauseErr := getExistingSession(sessionToUnpause.SessionID) + if unpauseErr != nil { + log.Println("pauseError", unpauseErr) + return + } + Sessions[unpausedSessionIdx].broadcast(unpauseTime) } // HandleRemoveUser ... of a disconneted user from the relevent session -func HandleRemoveUser(conn *websocket.Conn) (error) { +func HandleRemoveUser(conn *websocket.Conn) error { sessionIdx, userIdx, findConnErr := findUserByConn(conn) if findConnErr != nil { return findConnErr @@ -125,9 +176,9 @@ func HandleRemoveUser(conn *websocket.Conn) (error) { return nil } -func (session *Session) broadcastToSessionUsers() { - for _, user := range session.Users { - user.Conn.WriteJSON(session) +func (session *Session) broadcast(payload interface{}) { + for _, user := range session.Users { + user.Conn.WriteJSON(payload) } } @@ -146,7 +197,7 @@ func RemoveSession(sessionID string) error { // Map the incoming session request to an in-memory session func handleTimerEnd(session UpdateRequest) (int, error) { mappedSessionIdx, err := getExistingSession(session.SessionID) - if err != nil { + if err != nil { return -1, err } // update duration @@ -159,7 +210,7 @@ func handleTimerEnd(session UpdateRequest) (int, error) { } func getExistingSession(desiredSessionID string) (int, error) { - for idx, session := range Sessions { + for idx, session := range Sessions { if session.SessionID == desiredSessionID { return idx, nil } @@ -178,7 +229,7 @@ func (session *Session) selectNewDriver() { for _, user := range session.Users { if user.UUID != session.CurrentDriver.UUID { beenDriver := session.hasUserBeenDriver(user.UUID) - if beenDriver == false { + if !beenDriver { session.CurrentDriver = user session.PreviousDrivers = append( session.PreviousDrivers, @@ -190,14 +241,14 @@ func (session *Session) selectNewDriver() { } } } - func (session *Session) hasUserBeenDriver(uuid string) bool { - if len(session.PreviousDrivers) > 0 { - for _, prevDriver := range session.PreviousDrivers { - if uuid == prevDriver.UUID { - return true - } +func (session *Session) hasUserBeenDriver(uuid string) bool { + if len(session.PreviousDrivers) > 0 { + for _, prevDriver := range session.PreviousDrivers { + if uuid == prevDriver.UUID { + return true } } + } return false } @@ -208,8 +259,8 @@ func (session *Session) resetTimer() { } func (session *Session) addUser(user User) { - session.Users = append(session.Users, user) - } + session.Users = append(session.Users, user) +} func findSession(keyToFind interface{}) (int, error) { switch keyToFind.(type) { @@ -233,7 +284,7 @@ func findSession(keyToFind interface{}) (int, error) { } } } - return -1, errors.New("Cannot find Session") + return -1, errors.New("cannot find session") } func (session *Session) findUser(keyToFind interface{}) (int, error) { @@ -251,7 +302,7 @@ func (session *Session) findUser(keyToFind interface{}) (int, error) { } } } - return -1, errors.New("Cannot find user") + return -1, errors.New("cannot find user") } func (session *Session) removeUser(userIdx int) { @@ -267,7 +318,7 @@ func (session *Session) removeUser(userIdx int) { func (session *Session) resetCurrentDriver(userToBeRemoved User) { if userToBeRemoved == session.CurrentDriver { session.changeDriver() - session.broadcastToSessionUsers() + session.broadcast(session) } } diff --git a/session/session_test.go b/session/session_test.go index ca114a9..9609e7e 100644 --- a/session/session_test.go +++ b/session/session_test.go @@ -3,14 +3,15 @@ package session import ( "fmt" "testing" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/gorilla/websocket" + // "github.com/stretchr/testify/assert" "github.com/jaskaransarkaria/programming-timer-server/mocks" ) - type mockConnection struct { } @@ -30,7 +31,7 @@ func setup() (User, StartTimerReq, int, *mocks.Connector) { // mockUpgradeConn, _ := connToAdd.Upgrade() mockConn := &mocks.Connector{} var newSessionData = StartTimerReq{ - Duration: 60000, + Duration: 60000, StartTime: 1000, } mockConn.On("ReadMessage").Return(1, []byte("test byte array"), nil) @@ -58,7 +59,7 @@ func cleanup(sessionID string) { func TestCreateNewUserAndSession(t *testing.T) { var newSessionData = StartTimerReq{ - Duration: 60000, + Duration: 60000, StartTime: 1000, } var newUser = User{ @@ -66,12 +67,12 @@ func TestCreateNewUserAndSession(t *testing.T) { } var expected = Session{ - SessionID: "mocked-id-0", + SessionID: "mocked-id-0", CurrentDriver: newUser, - Duration: newSessionData.Duration, - StartTime: newSessionData.StartTime, - EndTime: newSessionData.Duration + newSessionData.StartTime, - Users: []User{newUser}, + Duration: newSessionData.Duration, + StartTime: newSessionData.StartTime, + EndTime: newSessionData.Duration + newSessionData.StartTime, + Users: []User{newUser}, } actual := CreateNewUserAndSession( @@ -117,24 +118,23 @@ func TestJoinExistingSession(t *testing.T) { var newUser = User{ UUID: "test-uuid2", } - + var sessionToJoin = ExistingSessionReq{ JoinSessionID: sessionID, } actual, err := JoinExistingSession(sessionToJoin, newUser) - if err != nil { t.Errorf("Expected: %+v but recieved: %+v", nil, err) } - + var expected = Session{ - SessionID: sessionID, + SessionID: sessionID, CurrentDriver: existingUser, - Duration: existingSessionData.Duration, - StartTime: existingSessionData.StartTime, - EndTime: existingSessionData.Duration + existingSessionData.StartTime, - Users: []User{existingUser, newUser}, + Duration: existingSessionData.Duration, + StartTime: existingSessionData.StartTime, + EndTime: existingSessionData.Duration + existingSessionData.StartTime, + Users: []User{existingUser, newUser}, } if !cmp.Equal(actual, expected, cmpopts.IgnoreFields(User{}, "Conn")) { @@ -147,11 +147,11 @@ func TestRemoveSession(t *testing.T) { _, _, sessionsLengthBeforeSessionCreated, _ := setup() sessionID := fmt.Sprintf("mocked-id-%d", sessionsLengthBeforeSessionCreated) removeSessionErr := RemoveSession(sessionID) - + if removeSessionErr != nil { t.Errorf("Expected nil but received %+v", removeSessionErr) } - + if len(Sessions) != sessionsLengthBeforeSessionCreated { t.Errorf("Expected %d sessions in Sessions slice but received %d\n with %+v", sessionsLengthBeforeSessionCreated, @@ -165,11 +165,75 @@ func TestRemoveSession(t *testing.T) { } } } + cleanup(sessionID) +} + +// TODO: the mocked session contains real timestamp +//func TestHandleUpdateSession(t *testing.T) { +// // take an existing session +// _, _, sessionIndex, mockConnInitUser := setup() +// // add another user (so we can verify that the function is switching driver correctly +// sessionID := fmt.Sprintf("mocked-id-%d", sessionIndex) +// mockConnJoiningUser := &mocks.Connector{} +// +// var newUser = User{ +// UUID: "test-uuid2", +// Conn: mockConnJoiningUser, +// } +// +// var sessionToJoin = ExistingSessionReq{ +// JoinSessionID: sessionID, +// } +// testSession, _ := JoinExistingSession(sessionToJoin, newUser) +// mockUpdateRequest := UpdateRequest{ +// SessionID: testSession.SessionID, +// UpdatedDuration: testSession.Duration, +// } +// // mock broadcast to all sessionUsers +// mockConnInitUser.On("WriteJSON", Sessions[sessionIndex]).Return(nil) +// mockConnJoiningUser.On("WriteJSON", Sessions[sessionIndex]).Return(nil) +// // fire handle time end (changes driver and resets the timer) +// HandleUpdateSession(mockUpdateRequest) +// +// if Sessions[sessionIndex].CurrentDriver.UUID != newUser.UUID { +// t.Errorf("The Driver has not been correctly changed") +// } +//} + +func TestHandlePauseSession(t *testing.T) { + // take an existing session + _, _, sessionIndex, mockConnInitUser := setup() + // add another user (so we can verify that the function is switching driver correctly + sessionID := fmt.Sprintf("mocked-id-%d", sessionIndex) + mockConnJoiningUser := &mocks.Connector{} + + var newUser = User{ + UUID: "test-uuid2", + Conn: mockConnJoiningUser, + } + + var sessionToJoin = ExistingSessionReq{ + JoinSessionID: sessionID, + } + testSession, _ := JoinExistingSession(sessionToJoin, newUser) + mockPauseRequest := PauseRequest{ + SessionID: testSession.SessionID, + PauseTime: testSession.EndTime - (testSession.Duration / 2), + } + + mockPauseResponse := PauseSessionResponse{ + PauseTime: mockPauseRequest.PauseTime, + } + // mock broadcast to all sessionUsers + mockConnInitUser.On("WriteJSON", mockPauseResponse).Return(nil) + mockConnJoiningUser.On("WriteJSON", mockPauseResponse).Return(nil) + + HandlePauseSession(mockPauseRequest) } -func TestHandleUpdateSession(t *testing.T) { +func TestHandleUnpauseSession(t *testing.T) { // take an existing session - _, _, sessionIndex, mockConnInitUser := setup(); + _, _, sessionIndex, mockConnInitUser := setup() // add another user (so we can verify that the function is switching driver correctly sessionID := fmt.Sprintf("mocked-id-%d", sessionIndex) mockConnJoiningUser := &mocks.Connector{} @@ -178,22 +242,36 @@ func TestHandleUpdateSession(t *testing.T) { UUID: "test-uuid2", Conn: mockConnJoiningUser, } - + var sessionToJoin = ExistingSessionReq{ JoinSessionID: sessionID, } testSession, _ := JoinExistingSession(sessionToJoin, newUser) - mockUpdateRequest := UpdateRequest{ + mockPauseRequest := PauseRequest{ SessionID: testSession.SessionID, - UpdatedDuration: testSession.Duration, + PauseTime: testSession.EndTime - (testSession.Duration / 2), + } + + mockPauseResponse := PauseSessionResponse{ + PauseTime: mockPauseRequest.PauseTime, } // mock broadcast to all sessionUsers - mockConnInitUser.On("WriteJSON", &Sessions[sessionIndex]).Return(nil) - mockConnJoiningUser.On("WriteJSON", &Sessions[sessionIndex]).Return(nil) - // fire handle time end (changes driver and resets the timer) - HandleUpdateSession(mockUpdateRequest) + mockConnInitUser.On("WriteJSON", mockPauseResponse).Return(nil) + mockConnJoiningUser.On("WriteJSON", mockPauseResponse).Return(nil) - if Sessions[sessionIndex].CurrentDriver.UUID != newUser.UUID { - t.Errorf("The Driver has not been correctly changed") + HandlePauseSession(mockPauseRequest) + + mockUnpauseRequest := UnpauseRequest{ + SessionID: sessionID, + UnpauseTime: testSession.EndTime - (testSession.Duration / 4), + } + + mockUnpauseResponse := UnpauseSessionResponse{ + UnpauseTime: testSession.EndTime - (testSession.Duration / 4), } + + mockConnInitUser.On("WriteJSON", mockUnpauseResponse).Return(nil) + mockConnJoiningUser.On("WriteJSON", mockUnpauseResponse).Return(nil) + HandleUnpauseSession(mockUnpauseRequest) + }