diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go index 9d0c233..888b6d8 100644 --- a/cmd/web/handlers.go +++ b/cmd/web/handlers.go @@ -184,6 +184,16 @@ func (app *application) createVotes(w http.ResponseWriter, r *http.Request) { app.unprocessableEntityErrorSingle(w, fmt.Errorf("you already voted")) return "" } + + voterExists, err := app.voters.Exists(voterIdentity, election.ID) + if err != nil { + app.serverError(w, r, err) + return "" + } + if !voterExists { + app.unprocessableEntityErrorSingle(w, fmt.Errorf("invalid voter identity")) + return "" + } } else { voterIdentity = r.RemoteAddr diff --git a/cmd/web/handlers_test.go b/cmd/web/handlers_test.go index c7cc86a..b7c7662 100644 --- a/cmd/web/handlers_test.go +++ b/cmd/web/handlers_test.go @@ -3,6 +3,8 @@ package main import ( "bytes" "code.dlmw.ch/dlmw/qv/internal" + "code.dlmw.ch/dlmw/qv/internal/models" + "database/sql" "encoding/json" "fmt" "github.com/stretchr/testify/assert" @@ -216,3 +218,265 @@ func TestCreateElection_ServerError(t *testing.T) { assert.Equal(t, 500, code) } + +func TestCreateVotesUnknownVotersElection(t *testing.T) { + app := newTestApplication(t) + server := newTestServer(t, app.routes()) + defer server.Close() + + mockElections := app.elections.(*mockElectionModel) + mockElections. + On("GetById", mock.Anything). + Return(&models.Election{ + ID: 1, + Name: "Guy of the year", + Tokens: 100, + AreVotersKnown: false, + MaxVoters: 100, + CreatedAt: time.Now(), + ExpiresAt: time.Now().Add(24 * time.Hour), + Choices: []string{"Gandhi", "Buddha"}, + }, nil) + + mockVoters := app.voters.(*mockVoterModel) + mockVoters. + On("Exists", mock.Anything, mock.Anything). + Return(false, nil) + mockVoters. + On("Insert", mock.Anything, mock.Anything). + Return(1, nil) + mockVoters. + On("CountByElection", mock.Anything). + Return(0, nil) + + mockVotes := app.votes.(*mockVoteModel) + mockVotes. + On("Insert", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(1, nil) + + path := "/votes" + + tests := []struct { + name string + urlPath string + body any + expectedCode int + }{ + { + name: "Valid request for unknown voters election", + urlPath: path, + body: api.CreateVotesRequest{ + Choices: []struct { + ChoiceText string `json:"choiceText"` + Tokens int `json:"tokens"` + }{ + {ChoiceText: "Gandhi", Tokens: 60}, + {ChoiceText: "Buddha", Tokens: 40}, + }, + ElectionId: 1, + VoterIdentity: nil, + }, + expectedCode: http.StatusCreated, + }, + { + name: "Invalid request for unknown voters election (too many tokens used)", + urlPath: path, + body: api.CreateVotesRequest{ + Choices: []struct { + ChoiceText string `json:"choiceText"` + Tokens int `json:"tokens"` + }{ + {ChoiceText: "Gandhi", Tokens: 60}, + {ChoiceText: "Buddha", Tokens: 41}, + }, + ElectionId: 1, + VoterIdentity: nil, + }, + expectedCode: http.StatusUnprocessableEntity, + }, + { + name: "Invalid request for unknown voters election (choice doesn't exist)", + urlPath: path, + body: api.CreateVotesRequest{ + Choices: []struct { + ChoiceText string `json:"choiceText"` + Tokens int `json:"tokens"` + }{ + {ChoiceText: "Gandhi", Tokens: 60}, + {ChoiceText: "Buddh", Tokens: 40}, + }, + ElectionId: 1, + VoterIdentity: nil, + }, + expectedCode: http.StatusUnprocessableEntity, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + requestBody, err := json.Marshal(tt.body) + if err != nil { + t.Fatal(err) + } + + code, _, _ := server.post(t, tt.urlPath, bytes.NewReader(requestBody)) + assert.Equal(t, tt.expectedCode, code) + }) + } +} + +func TestCreateVotesKnownVotersElection(t *testing.T) { + app := newTestApplication(t) + server := newTestServer(t, app.routes()) + defer server.Close() + + mockElections := app.elections.(*mockElectionModel) + mockElections. + On("GetById", mock.Anything). + Return(&models.Election{ + ID: 1, + Name: "Guy of the year", + Tokens: 100, + AreVotersKnown: true, + MaxVoters: 100, + CreatedAt: time.Now(), + ExpiresAt: time.Now().Add(24 * time.Hour), + Choices: []string{"Gandhi", "Buddha"}, + }, nil) + + EXISTING_VOTER_IDENTITY := "EXISTING_VOTER_IDENTITY" + NON_EXISTING_VOTER_IDENTITY := "NON_EXISTING_VOTER_IDENTITY" + mockVoters := app.voters.(*mockVoterModel) + mockVoters. + On("Exists", EXISTING_VOTER_IDENTITY, mock.Anything). + Return(true, nil) + mockVoters. + On("Exists", NON_EXISTING_VOTER_IDENTITY, mock.Anything). + Return(false, nil) + + mockVotes := app.votes.(*mockVoteModel) + mockVotes. + On("Insert", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(1, nil) + mockVotes. + On("Exists", mock.Anything, mock.Anything). + Return(false, nil) + + path := "/votes" + + tests := []struct { + name string + urlPath string + body any + expectedCode int + }{ + { + name: "Valid request for unknown voters election", + urlPath: path, + body: api.CreateVotesRequest{ + Choices: []struct { + ChoiceText string `json:"choiceText"` + Tokens int `json:"tokens"` + }{ + {ChoiceText: "Gandhi", Tokens: 60}, + {ChoiceText: "Buddha", Tokens: 40}, + }, + ElectionId: 1, + VoterIdentity: &EXISTING_VOTER_IDENTITY, + }, + expectedCode: http.StatusCreated, + }, + { + name: "Invalid request for unknown voters election (non-existing voter identity)", + urlPath: path, + body: api.CreateVotesRequest{ + Choices: []struct { + ChoiceText string `json:"choiceText"` + Tokens int `json:"tokens"` + }{ + {ChoiceText: "Gandhi", Tokens: 60}, + {ChoiceText: "Buddha", Tokens: 40}, + }, + ElectionId: 1, + VoterIdentity: &NON_EXISTING_VOTER_IDENTITY, + }, + expectedCode: http.StatusUnprocessableEntity, + }, + { + name: "Invalid request for unknown voters election (too many tokens used)", + urlPath: path, + body: api.CreateVotesRequest{ + Choices: []struct { + ChoiceText string `json:"choiceText"` + Tokens int `json:"tokens"` + }{ + {ChoiceText: "Gandhi", Tokens: 60}, + {ChoiceText: "Buddha", Tokens: 41}, + }, + ElectionId: 1, + VoterIdentity: &EXISTING_VOTER_IDENTITY, + }, + expectedCode: http.StatusUnprocessableEntity, + }, + { + name: "Invalid request for unknown voters election (choice doesn't exist)", + urlPath: path, + body: api.CreateVotesRequest{ + Choices: []struct { + ChoiceText string `json:"choiceText"` + Tokens int `json:"tokens"` + }{ + {ChoiceText: "Gandhi", Tokens: 60}, + {ChoiceText: "Buddh", Tokens: 40}, + }, + ElectionId: 1, + VoterIdentity: &EXISTING_VOTER_IDENTITY, + }, + expectedCode: http.StatusUnprocessableEntity, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + requestBody, err := json.Marshal(tt.body) + if err != nil { + t.Fatal(err) + } + + code, _, _ := server.post(t, tt.urlPath, bytes.NewReader(requestBody)) + assert.Equal(t, tt.expectedCode, code) + }) + } +} + +func TestCreateVotes_NonExistingElection(t *testing.T) { + app := newTestApplication(t) + server := newTestServer(t, app.routes()) + defer server.Close() + + mockElections := app.elections.(*mockElectionModel) + mockElections. + On("GetById", mock.Anything). + Return((*models.Election)(nil), sql.ErrNoRows) + + path := "/votes" + requestBody := api.CreateVotesRequest{ + Choices: []struct { + ChoiceText string `json:"choiceText"` + Tokens int `json:"tokens"` + }{ + {ChoiceText: "Gandhi", Tokens: 60}, + {ChoiceText: "Buddha", Tokens: 40}, + }, + ElectionId: 1, + VoterIdentity: nil, + } + requestBodyJson, err := json.Marshal(requestBody) + if err != nil { + t.Fatal(err) + } + + code, _, _ := server.post(t, path, bytes.NewReader(requestBodyJson)) + + assert.Equal(t, http.StatusUnprocessableEntity, code) +} diff --git a/cmd/web/testutils_test.go b/cmd/web/testutils_test.go index d197f10..9683202 100644 --- a/cmd/web/testutils_test.go +++ b/cmd/web/testutils_test.go @@ -54,30 +54,27 @@ func (e *mockElectionModel) Insert(name string, tokens int, areVotersKnown bool, } func (e *mockElectionModel) GetById(id int) (*models.Election, error) { - return &models.Election{ - ID: id, - Name: "Guy of the year", - Tokens: 100, - AreVotersKnown: false, - MaxVoters: 10, - CreatedAt: time.Now(), - ExpiresAt: time.Now().Add(100 * time.Hour), - }, nil + args := e.Called(id) + return args.Get(0).(*models.Election), args.Error(1) } type mockVoterModel struct { + mock.Mock } func (v *mockVoterModel) Insert(identity string, electionID int) (int, error) { - return 1, nil + args := v.Called(identity, electionID) + return args.Int(0), args.Error(1) } func (v *mockVoterModel) CountByElection(electionID int) (int, error) { - return 10, nil + args := v.Called(electionID) + return args.Int(0), args.Error(1) } func (v *mockVoterModel) Exists(voterIdentity string, electionID int) (bool, error) { - return true, nil + args := v.Called(voterIdentity, electionID) + return args.Bool(0), args.Error(1) } type mockVoteModel struct { @@ -90,5 +87,6 @@ func (v *mockVoteModel) Insert(voterIdentity string, electionId int, choiceText } func (v *mockVoteModel) Exists(voterIdentity string, electionID int) (bool, error) { - return true, nil + args := v.Called(voterIdentity, electionID) + return args.Bool(0), args.Error(1) }