From 782210fa1197a0d2b36ae3996842c91a30bb411a Mon Sep 17 00:00:00 2001 From: Justin Farrell Date: Fri, 22 Nov 2024 18:38:53 -0500 Subject: [PATCH 1/7] Change lobby ID type from string to int64 and add unit tests for GetLobby handler --- internal/lobby/lobby.go | 4 +- internal/lobby/lobby_test.go | 117 +++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 internal/lobby/lobby_test.go diff --git a/internal/lobby/lobby.go b/internal/lobby/lobby.go index 3c3c9ea..b83f7ab 100644 --- a/internal/lobby/lobby.go +++ b/internal/lobby/lobby.go @@ -5,7 +5,7 @@ package lobby // @Description Structure for representing a player lobby. type Lobby struct { // ID is the unique identifier for the lobby. - ID string `json:"id,omitempty"` + ID int64 `json:"id,omitempty"` // Name is the name of the lobby. Name string `json:"name"` @@ -28,7 +28,7 @@ type Lobby struct { // @Description Structure for representing a player lobby with non-required fields. type LobbyParam struct { // ID is the unique identifier for the lobby. - ID *string `json:"id,omitempty"` + ID *int64 `json:"id,omitempty"` // Name is the name of the lobby. Name *string `json:"name,omitempty"` diff --git a/internal/lobby/lobby_test.go b/internal/lobby/lobby_test.go new file mode 100644 index 0000000..1b081d2 --- /dev/null +++ b/internal/lobby/lobby_test.go @@ -0,0 +1,117 @@ +package lobby + +import ( + "database/sql" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/jmoiron/sqlx" +) + +func TestGetLobby_NotFound(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + lobbyID := int8(1) + + mock.ExpectQuery("SELECT id, name, owner_name, is_closed, is_muted, is_public FROM lobby WHERE id = \\$1"). + WithArgs(lobbyID). + WillReturnError(sql.ErrNoRows) + + req, err := http.NewRequest("GET", "/lobby/get_lobby", strings.NewReader(`{"lobby_id": 1}`)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + sqlxDB := sqlx.NewDb(db, "sqlmock") + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := GetLobby(w, r, sqlxDB) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusInternalServerError { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusInternalServerError) + } + + expectedError := "no lobby exists with the ID 1" + if strings.TrimSpace(rr.Body.String()) != expectedError { + t.Errorf("handler returned unexpected body: got %v want %v", strings.TrimSpace(rr.Body.String()), expectedError) + } +} + +func TestGetLobby_InvalidMethod(t *testing.T) { + db, _, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + req, err := http.NewRequest("POST", "/lobby/get_lobby", strings.NewReader(`{"lobby_id": 1}`)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + sqlxDB := sqlx.NewDb(db, "sqlmock") + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := GetLobby(w, r, sqlxDB) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusInternalServerError { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusInternalServerError) + } + + expectedError := "invalid request; request must be a GET request" + if strings.TrimSpace(rr.Body.String()) != expectedError { + t.Errorf("handler returned unexpected body: got %v want %v", strings.TrimSpace(rr.Body.String()), expectedError) + } +} + +func TestGetLobby_DecodeError(t *testing.T) { + db, _, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + req, err := http.NewRequest("GET", "/lobby/get_lobby", strings.NewReader(`{"lobby_id": "invalid"}`)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + sqlxDB := sqlx.NewDb(db, "sqlmock") + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := GetLobby(w, r, sqlxDB) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusInternalServerError { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusInternalServerError) + } + + expectedError := "an error occurred while decoding the request body: json: cannot unmarshal string into Go struct field GetLobbyArgs.lobby_id of type int8" + if strings.TrimSpace(rr.Body.String()) != expectedError { + t.Errorf("handler returned unexpected body: got %v want %v", strings.TrimSpace(rr.Body.String()), expectedError) + } +} From c5d7bb116e5d2d3970707dbee0db57ecc7b7d6dc Mon Sep 17 00:00:00 2001 From: Justin Farrell Date: Fri, 22 Nov 2024 18:39:00 -0500 Subject: [PATCH 2/7] Update lobby ID type from string to integer in documentation --- docs/docs.go | 2 +- docs/swagger.json | 2 +- docs/swagger.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 09a75a7..c4fedae 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -493,7 +493,7 @@ const docTemplate = `{ "properties": { "id": { "description": "ID is the unique identifier for the lobby.", - "type": "string" + "type": "integer" }, "is_closed": { "description": "IsClosed indicates if the lobby is closed.", diff --git a/docs/swagger.json b/docs/swagger.json index 00e6e14..ab97885 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -484,7 +484,7 @@ "properties": { "id": { "description": "ID is the unique identifier for the lobby.", - "type": "string" + "type": "integer" }, "is_closed": { "description": "IsClosed indicates if the lobby is closed.", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index fb95d65..60328b8 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -128,7 +128,7 @@ definitions: properties: id: description: ID is the unique identifier for the lobby. - type: string + type: integer is_closed: description: IsClosed indicates if the lobby is closed. type: boolean From 3f360502bd6a58a867d4cd56b6b7c4b0402720dd Mon Sep 17 00:00:00 2001 From: Justin Farrell Date: Fri, 22 Nov 2024 18:44:28 -0500 Subject: [PATCH 3/7] Add database tags to Lobby and LobbyParam structs and implement GetLobby unit test --- internal/lobby/lobby.go | 24 +++++++++--------- internal/lobby/lobby_test.go | 48 ++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/internal/lobby/lobby.go b/internal/lobby/lobby.go index b83f7ab..0527cce 100644 --- a/internal/lobby/lobby.go +++ b/internal/lobby/lobby.go @@ -5,22 +5,22 @@ package lobby // @Description Structure for representing a player lobby. type Lobby struct { // ID is the unique identifier for the lobby. - ID int64 `json:"id,omitempty"` + ID int64 `json:"id,omitempty" db:"id"` // Name is the name of the lobby. - Name string `json:"name"` + Name string `json:"name" db:"name"` // OwnerName is the name of the lobby owner. - OwnerName string `json:"owner_name"` + OwnerName string `json:"owner_name" db:"owner_name"` // IsClosed indicates if the lobby is closed. - IsClosed bool `json:"is_closed"` + IsClosed bool `json:"is_closed" db:"is_closed"` // IsMuted indicates if the lobby is muted. - IsMuted bool `json:"is_muted"` + IsMuted bool `json:"is_muted" db:"is_muted"` // IsPublic indicates if the lobby is public. - IsPublic bool `json:"is_public"` + IsPublic bool `json:"is_public" db:"is_public"` } // LobbyParam represents a player lobby with non-required fields. @@ -28,20 +28,20 @@ type Lobby struct { // @Description Structure for representing a player lobby with non-required fields. type LobbyParam struct { // ID is the unique identifier for the lobby. - ID *int64 `json:"id,omitempty"` + ID *int64 `json:"id,omitempty" db:"id"` // Name is the name of the lobby. - Name *string `json:"name,omitempty"` + Name *string `json:"name,omitempty" db:"name"` // OwnerName is the name of the lobby owner. - OwnerName *string `json:"owner_name,omitempty"` + OwnerName *string `json:"owner_name,omitempty" db:"owner_name"` // IsClosed indicates if the lobby is closed. - IsClosed *bool `json:"is_closed,omitempty"` + IsClosed *bool `json:"is_closed,omitempty" db:"is_closed"` // IsMuted indicates if the lobby is muted. - IsMuted *bool `json:"is_muted,omitempty"` + IsMuted *bool `json:"is_muted,omitempty" db:"is_muted"` // IsPublic indicates if the lobby is public. - IsPublic *bool `json:"is_public,omitempty"` + IsPublic *bool `json:"is_public,omitempty" db:"is_public"` } diff --git a/internal/lobby/lobby_test.go b/internal/lobby/lobby_test.go index 1b081d2..b0ebcd4 100644 --- a/internal/lobby/lobby_test.go +++ b/internal/lobby/lobby_test.go @@ -11,6 +11,54 @@ import ( "github.com/jmoiron/sqlx" ) +func TestGetLobby_Success(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + lobbyID := int64(1) + expectedLobby := Lobby{ + ID: lobbyID, + Name: "Test Lobby", + OwnerName: "Owner", + IsClosed: false, + IsMuted: false, + IsPublic: true, + } + + mock.ExpectQuery("SELECT id, name, owner_name, is_closed, is_muted, is_public FROM lobby WHERE id = \\$1"). + WithArgs(lobbyID). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "owner_name", "is_closed", "is_muted", "is_public"}). + AddRow(expectedLobby.ID, expectedLobby.Name, expectedLobby.OwnerName, expectedLobby.IsClosed, expectedLobby.IsMuted, expectedLobby.IsPublic)) + + req, err := http.NewRequest("GET", "/lobby/get_lobby", strings.NewReader(`{"lobby_id": 1}`)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + sqlxDB := sqlx.NewDb(db, "sqlmock") + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := GetLobby(w, r, sqlxDB) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + + expectedResponse := `{"id":1,"name":"Test Lobby","owner_name":"Owner","is_closed":false,"is_muted":false,"is_public":true}` + if strings.TrimSpace(rr.Body.String()) != expectedResponse { + t.Errorf("handler returned unexpected body: got %v want %v", strings.TrimSpace(rr.Body.String()), expectedResponse) + } +} + func TestGetLobby_NotFound(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { From 30350d75864884623435bcd8a8fcd1494d503fbf Mon Sep 17 00:00:00 2001 From: Justin Farrell Date: Fri, 22 Nov 2024 18:46:15 -0500 Subject: [PATCH 4/7] Moved account_test.go to get_account_test.go --- internal/account/{account_test.go => get_account_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/account/{account_test.go => get_account_test.go} (100%) diff --git a/internal/account/account_test.go b/internal/account/get_account_test.go similarity index 100% rename from internal/account/account_test.go rename to internal/account/get_account_test.go From 770365b9d112abc10f4a023f8f35d46efc2b1a6d Mon Sep 17 00:00:00 2001 From: Justin Farrell Date: Fri, 22 Nov 2024 18:47:25 -0500 Subject: [PATCH 5/7] Moved lobby_test.go to get_lobby_test.go --- internal/lobby/{lobby_test.go => get_lobby_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/lobby/{lobby_test.go => get_lobby_test.go} (100%) diff --git a/internal/lobby/lobby_test.go b/internal/lobby/get_lobby_test.go similarity index 100% rename from internal/lobby/lobby_test.go rename to internal/lobby/get_lobby_test.go From 0e97e431d168bdd00e51b830fad9bb79adf06538 Mon Sep 17 00:00:00 2001 From: Justin Farrell Date: Fri, 22 Nov 2024 19:02:10 -0500 Subject: [PATCH 6/7] Add unit tests for CreateLobby handler with various error scenarios --- internal/lobby/create_lobby_test.go | 143 ++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 internal/lobby/create_lobby_test.go diff --git a/internal/lobby/create_lobby_test.go b/internal/lobby/create_lobby_test.go new file mode 100644 index 0000000..1cd115f --- /dev/null +++ b/internal/lobby/create_lobby_test.go @@ -0,0 +1,143 @@ +package lobby + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/jmoiron/sqlx" +) + +func TestCreateLobby_InvalidMethod(t *testing.T) { + db, _, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + req, err := http.NewRequest("GET", "/lobby/create_lobby", strings.NewReader(`{"lobby": {"name": "Test Lobby", "owner_name": "Owner", "is_closed": false, "is_muted": false, "is_public": true}, "password": "password123"}`)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + sqlxDB := sqlx.NewDb(db, "sqlmock") + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := CreateLobby(w, r, sqlxDB) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusInternalServerError { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusInternalServerError) + } + + expectedError := "invalid request; request must be a POST request" + if strings.TrimSpace(rr.Body.String()) != expectedError { + t.Errorf("handler returned unexpected body: got %v want %v", strings.TrimSpace(rr.Body.String()), expectedError) + } +} + +func TestCreateLobby_DecodeError(t *testing.T) { + db, _, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + req, err := http.NewRequest("POST", "/lobby/create_lobby", strings.NewReader(`{"lobby": {"name": "Test Lobby", "owner_name": "Owner", "is_closed": false, "is_muted": false, "is_public": true}, "password": 123}`)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + sqlxDB := sqlx.NewDb(db, "sqlmock") + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := CreateLobby(w, r, sqlxDB) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusInternalServerError { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusInternalServerError) + } + + expectedError := "an error occurred while decoding the request body:json: cannot unmarshal number into Go struct field CreateLobbyArgs.password of type string" + if strings.TrimSpace(rr.Body.String()) != expectedError { + t.Errorf("handler returned unexpected body: got %v want %v", strings.TrimSpace(rr.Body.String()), expectedError) + } +} + +func TestCreateLobby_PasswordTooShort(t *testing.T) { + db, _, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + req, err := http.NewRequest("POST", "/lobby/create_lobby", strings.NewReader(`{"lobby": {"name": "Test Lobby", "owner_name": "Owner", "is_closed": false, "is_muted": false, "is_public": true}, "password": "123"}`)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + sqlxDB := sqlx.NewDb(db, "sqlmock") + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := CreateLobby(w, r, sqlxDB) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + }) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusBadRequest { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusBadRequest) + } + + expectedError := ERROR_PASSWORD_TOO_SHORT + if strings.TrimSpace(rr.Body.String()) != expectedError { + t.Errorf("handler returned unexpected body: got %v want %v", strings.TrimSpace(rr.Body.String()), expectedError) + } +} + +func TestCreateLobby_PasswordRequired(t *testing.T) { + db, _, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + req, err := http.NewRequest("POST", "/lobby/create_lobby", strings.NewReader(`{"lobby": {"name": "Test Lobby", "owner_name": "Owner", "is_closed": false, "is_muted": false, "is_public": true}}`)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + sqlxDB := sqlx.NewDb(db, "sqlmock") + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := CreateLobby(w, r, sqlxDB) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + }) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusBadRequest { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusBadRequest) + } + + expectedError := ERROR_PASSWORD_REQUIRED_BUT_NO_PASSWORD + if strings.TrimSpace(rr.Body.String()) != expectedError { + t.Errorf("handler returned unexpected body: got %v want %v", strings.TrimSpace(rr.Body.String()), expectedError) + } +} From 32672b207aff559714cdc87b179bb247e5a5211a Mon Sep 17 00:00:00 2001 From: Justin Farrell Date: Fri, 22 Nov 2024 19:04:29 -0500 Subject: [PATCH 7/7] Add unit test for CreateLobby handler to verify successful lobby creation --- internal/lobby/create_lobby_test.go | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/internal/lobby/create_lobby_test.go b/internal/lobby/create_lobby_test.go index 1cd115f..d292729 100644 --- a/internal/lobby/create_lobby_test.go +++ b/internal/lobby/create_lobby_test.go @@ -10,6 +10,46 @@ import ( "github.com/jmoiron/sqlx" ) +func TestCreateLobby_Success(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + lobby := Lobby{ + Name: "Test Lobby", + OwnerName: "Owner", + IsClosed: false, + IsMuted: false, + IsPublic: true, + } + + mock.ExpectQuery("INSERT INTO lobby \\(name, owner_name, is_closed, is_muted, is_public\\) VALUES \\(\\$1, \\$2, \\$3, \\$4, \\$5\\)"). + WithArgs(lobby.Name, lobby.OwnerName, lobby.IsClosed, lobby.IsMuted, lobby.IsPublic). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1)) + + req, err := http.NewRequest("POST", "/lobby/create_lobby", strings.NewReader(`{"lobby": {"name": "Test Lobby", "owner_name": "Owner", "is_closed": false, "is_muted": false, "is_public": true}, "password": "password123"}`)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + sqlxDB := sqlx.NewDb(db, "sqlmock") + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := CreateLobby(w, r, sqlxDB) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusCreated { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusCreated) + } +} + func TestCreateLobby_InvalidMethod(t *testing.T) { db, _, err := sqlmock.New() if err != nil {