Move voting to a new endpoint
/election/{id}/votes
This commit is contained in:
@ -11,6 +11,7 @@ import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -111,8 +112,6 @@ type createVotesRequestWithValidator struct {
|
||||
}
|
||||
|
||||
func (r *createVotesRequestWithValidator) isValid() bool {
|
||||
r.CheckField(validator.GreaterThan(r.ElectionId, 0), "electionId", "must be greater than 0")
|
||||
|
||||
for _, choice := range r.Choices {
|
||||
r.CheckField(validator.NotBlank(choice.ChoiceText), "choiceText", "must not be blank")
|
||||
}
|
||||
@ -133,10 +132,16 @@ func (app *application) createVotes(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
election, err := app.elections.GetById(request.ElectionId) // get from path instead of from the JSON
|
||||
electionID, err := strconv.Atoi(r.PathValue("id"))
|
||||
if err != nil {
|
||||
app.clientError(w, http.StatusBadRequest, "Couldn't convert the id you provided to a number")
|
||||
return
|
||||
}
|
||||
|
||||
election, err := app.elections.GetById(electionID)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
app.unprocessableEntityErrorSingle(w, fmt.Errorf("election with id %v doesn't exist", request.ElectionId)) // TODO: return 404
|
||||
app.clientError(w, http.StatusNotFound, "Couldn't find an election with the ID you provided")
|
||||
return
|
||||
}
|
||||
app.serverError(w, r, err)
|
||||
@ -145,7 +150,7 @@ func (app *application) createVotes(w http.ResponseWriter, r *http.Request) {
|
||||
for _, c := range request.Choices {
|
||||
choiceExists := slices.Contains(election.Choices, c.ChoiceText)
|
||||
if !choiceExists {
|
||||
app.unprocessableEntityErrorSingle(w, fmt.Errorf("choice %v doesn't exist", c.ChoiceText))
|
||||
app.clientError(w, http.StatusUnprocessableEntity, fmt.Sprintf("choice %v doesn't exist", c.ChoiceText))
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -156,13 +161,13 @@ func (app *application) createVotes(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if tokensUsed > election.Tokens {
|
||||
app.unprocessableEntityErrorSingle(w, fmt.Errorf("you used too many tokens; must not exceed %v tokens", election.Tokens))
|
||||
app.clientError(w, http.StatusUnprocessableEntity, fmt.Sprintf("you used too many tokens; must not exceed %v tokens", election.Tokens))
|
||||
return
|
||||
}
|
||||
|
||||
electionHasExpired := election.ExpiresAt.Before(time.Now())
|
||||
if electionHasExpired {
|
||||
app.unprocessableEntityErrorSingle(w, fmt.Errorf("election has expired"))
|
||||
app.clientError(w, http.StatusUnprocessableEntity, "election has expired")
|
||||
return
|
||||
}
|
||||
|
||||
@ -193,9 +198,9 @@ func (app *application) createVotes(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (app *application) createVotesHandleKnownVotersElection(w http.ResponseWriter, r *http.Request, request createVotesRequestWithValidator, election *models.Election) (string, error) {
|
||||
if request.VoterIdentity == nil || validator.Blank(*request.VoterIdentity) {
|
||||
err := fmt.Errorf("election has known voters; you must provide an identity provided by the organizer")
|
||||
app.unprocessableEntityErrorSingle(w, err)
|
||||
return "", err
|
||||
message := "election has known voters; you must provide an identity provided by the organizer"
|
||||
app.clientError(w, http.StatusUnprocessableEntity, message)
|
||||
return "", fmt.Errorf(message)
|
||||
}
|
||||
|
||||
voterIdentity := *request.VoterIdentity
|
||||
@ -205,9 +210,9 @@ func (app *application) createVotesHandleKnownVotersElection(w http.ResponseWrit
|
||||
return "", err
|
||||
}
|
||||
if hasCastVotes {
|
||||
err := fmt.Errorf("you already voted")
|
||||
app.unprocessableEntityErrorSingle(w, err)
|
||||
return "", err
|
||||
message := "you already voted"
|
||||
app.clientError(w, http.StatusUnprocessableEntity, message)
|
||||
return "", fmt.Errorf(message)
|
||||
}
|
||||
|
||||
voterExists, err := app.voters.Exists(voterIdentity, election.ID)
|
||||
@ -216,9 +221,9 @@ func (app *application) createVotesHandleKnownVotersElection(w http.ResponseWrit
|
||||
return "", err
|
||||
}
|
||||
if !voterExists {
|
||||
err := fmt.Errorf("invalid voter identity")
|
||||
app.unprocessableEntityErrorSingle(w, err)
|
||||
return "", err
|
||||
message := "invalid voter identity"
|
||||
app.clientError(w, http.StatusUnprocessableEntity, message)
|
||||
return "", fmt.Errorf(message)
|
||||
}
|
||||
|
||||
return voterIdentity, nil
|
||||
@ -233,9 +238,9 @@ func (app *application) createVotesHandleUnknownVotersElection(w http.ResponseWr
|
||||
return "", err
|
||||
}
|
||||
if voterExists {
|
||||
err := fmt.Errorf("you already voted")
|
||||
app.unprocessableEntityErrorSingle(w, err)
|
||||
return "", err
|
||||
message := "you already voted"
|
||||
app.clientError(w, http.StatusUnprocessableEntity, message)
|
||||
return "", fmt.Errorf(message)
|
||||
}
|
||||
|
||||
_, err = app.voters.Insert(voterIdentity, election.ID)
|
||||
@ -250,9 +255,9 @@ func (app *application) createVotesHandleUnknownVotersElection(w http.ResponseWr
|
||||
return "", err
|
||||
}
|
||||
if voterCount == election.MaxVoters {
|
||||
err := fmt.Errorf("maximum voters reached")
|
||||
app.unprocessableEntityErrorSingle(w, err)
|
||||
return "", err
|
||||
message := "maximum voters reached"
|
||||
app.clientError(w, http.StatusUnprocessableEntity, message)
|
||||
return "", fmt.Errorf(message)
|
||||
}
|
||||
|
||||
return voterIdentity, nil
|
||||
|
@ -254,7 +254,7 @@ func TestCreateVotes_UnknownVotersElection(t *testing.T) {
|
||||
On("Insert", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
|
||||
Return(1, nil)
|
||||
|
||||
path := "/votes"
|
||||
path := "/election/1/votes"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -273,7 +273,6 @@ func TestCreateVotes_UnknownVotersElection(t *testing.T) {
|
||||
{ChoiceText: "Gandhi", Tokens: 60},
|
||||
{ChoiceText: "Buddha", Tokens: 40},
|
||||
},
|
||||
ElectionId: 1,
|
||||
VoterIdentity: nil,
|
||||
},
|
||||
expectedCode: http.StatusCreated,
|
||||
@ -289,7 +288,6 @@ func TestCreateVotes_UnknownVotersElection(t *testing.T) {
|
||||
{ChoiceText: "Gandhi", Tokens: 60},
|
||||
{ChoiceText: "Buddha", Tokens: 41},
|
||||
},
|
||||
ElectionId: 1,
|
||||
VoterIdentity: nil,
|
||||
},
|
||||
expectedCode: http.StatusUnprocessableEntity,
|
||||
@ -305,7 +303,6 @@ func TestCreateVotes_UnknownVotersElection(t *testing.T) {
|
||||
{ChoiceText: "Gandhi", Tokens: 60},
|
||||
{ChoiceText: "Buddh", Tokens: 40},
|
||||
},
|
||||
ElectionId: 1,
|
||||
VoterIdentity: nil,
|
||||
},
|
||||
expectedCode: http.StatusUnprocessableEntity,
|
||||
@ -362,7 +359,7 @@ func TestCreateVotes_KnownVotersElection(t *testing.T) {
|
||||
On("Exists", mock.Anything, mock.Anything).
|
||||
Return(false, nil)
|
||||
|
||||
path := "/votes"
|
||||
path := "/election/1/votes"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -381,7 +378,6 @@ func TestCreateVotes_KnownVotersElection(t *testing.T) {
|
||||
{ChoiceText: "Gandhi", Tokens: 60},
|
||||
{ChoiceText: "Buddha", Tokens: 40},
|
||||
},
|
||||
ElectionId: 1,
|
||||
VoterIdentity: &EXISTING_VOTER_IDENTITY,
|
||||
},
|
||||
expectedCode: http.StatusCreated,
|
||||
@ -397,7 +393,6 @@ func TestCreateVotes_KnownVotersElection(t *testing.T) {
|
||||
{ChoiceText: "Gandhi", Tokens: 60},
|
||||
{ChoiceText: "Buddha", Tokens: 40},
|
||||
},
|
||||
ElectionId: 1,
|
||||
VoterIdentity: &NON_EXISTING_VOTER_IDENTITY,
|
||||
},
|
||||
expectedCode: http.StatusUnprocessableEntity,
|
||||
@ -413,7 +408,6 @@ func TestCreateVotes_KnownVotersElection(t *testing.T) {
|
||||
{ChoiceText: "Gandhi", Tokens: 60},
|
||||
{ChoiceText: "Buddha", Tokens: 40},
|
||||
},
|
||||
ElectionId: 1,
|
||||
VoterIdentity: nil,
|
||||
},
|
||||
expectedCode: http.StatusUnprocessableEntity,
|
||||
@ -429,7 +423,6 @@ func TestCreateVotes_KnownVotersElection(t *testing.T) {
|
||||
{ChoiceText: "Gandhi", Tokens: 60},
|
||||
{ChoiceText: "Buddha", Tokens: 41},
|
||||
},
|
||||
ElectionId: 1,
|
||||
VoterIdentity: &EXISTING_VOTER_IDENTITY,
|
||||
},
|
||||
expectedCode: http.StatusUnprocessableEntity,
|
||||
@ -445,7 +438,6 @@ func TestCreateVotes_KnownVotersElection(t *testing.T) {
|
||||
{ChoiceText: "Gandhi", Tokens: 60},
|
||||
{ChoiceText: "Buddh", Tokens: 40},
|
||||
},
|
||||
ElectionId: 1,
|
||||
VoterIdentity: &EXISTING_VOTER_IDENTITY,
|
||||
},
|
||||
expectedCode: http.StatusUnprocessableEntity,
|
||||
@ -475,7 +467,7 @@ func TestCreateVotes_NonExistingElection(t *testing.T) {
|
||||
On("GetById", mock.Anything).
|
||||
Return((*models.Election)(nil), sql.ErrNoRows)
|
||||
|
||||
path := "/votes"
|
||||
path := "/election/1/votes"
|
||||
requestBody := api.CreateVotesRequest{
|
||||
Choices: []struct {
|
||||
ChoiceText string `json:"choiceText"`
|
||||
@ -484,7 +476,6 @@ func TestCreateVotes_NonExistingElection(t *testing.T) {
|
||||
{ChoiceText: "Gandhi", Tokens: 60},
|
||||
{ChoiceText: "Buddha", Tokens: 40},
|
||||
},
|
||||
ElectionId: 1,
|
||||
VoterIdentity: nil,
|
||||
}
|
||||
requestBodyJson, err := json.Marshal(requestBody)
|
||||
@ -494,7 +485,38 @@ func TestCreateVotes_NonExistingElection(t *testing.T) {
|
||||
|
||||
code, _, _ := server.post(t, path, bytes.NewReader(requestBodyJson))
|
||||
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, code)
|
||||
assert.Equal(t, http.StatusNotFound, code)
|
||||
}
|
||||
|
||||
func TestCreateVotes_NonNumberElectionID(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 := "/election/1a/votes"
|
||||
requestBody := api.CreateVotesRequest{
|
||||
Choices: []struct {
|
||||
ChoiceText string `json:"choiceText"`
|
||||
Tokens int `json:"tokens"`
|
||||
}{
|
||||
{ChoiceText: "Gandhi", Tokens: 60},
|
||||
{ChoiceText: "Buddha", Tokens: 40},
|
||||
},
|
||||
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.StatusBadRequest, code)
|
||||
}
|
||||
|
||||
func TestCreateVotes_AlreadyVoted(t *testing.T) {
|
||||
@ -533,7 +555,8 @@ func TestCreateVotes_AlreadyVoted(t *testing.T) {
|
||||
On("Exists", mock.Anything, mock.Anything).
|
||||
Return(true, nil)
|
||||
|
||||
path := "/votes"
|
||||
existingElectionPath := "/election/1/votes"
|
||||
nonExistingElectionPath := "/election/1/votes"
|
||||
voterIdentity := "anything"
|
||||
|
||||
tests := []struct {
|
||||
@ -544,7 +567,7 @@ func TestCreateVotes_AlreadyVoted(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "Invalid request for known voters election (already voted)",
|
||||
urlPath: path,
|
||||
urlPath: existingElectionPath,
|
||||
body: api.CreateVotesRequest{
|
||||
Choices: []struct {
|
||||
ChoiceText string `json:"choiceText"`
|
||||
@ -553,14 +576,13 @@ func TestCreateVotes_AlreadyVoted(t *testing.T) {
|
||||
{ChoiceText: "Gandhi", Tokens: 60},
|
||||
{ChoiceText: "Buddha", Tokens: 40},
|
||||
},
|
||||
ElectionId: 1,
|
||||
VoterIdentity: &voterIdentity,
|
||||
},
|
||||
expectedCode: http.StatusUnprocessableEntity,
|
||||
},
|
||||
{
|
||||
name: "Invalid request for unknown voters election (already voted)",
|
||||
urlPath: path,
|
||||
urlPath: nonExistingElectionPath,
|
||||
body: api.CreateVotesRequest{
|
||||
Choices: []struct {
|
||||
ChoiceText string `json:"choiceText"`
|
||||
@ -569,7 +591,6 @@ func TestCreateVotes_AlreadyVoted(t *testing.T) {
|
||||
{ChoiceText: "Gandhi", Tokens: 60},
|
||||
{ChoiceText: "Buddha", Tokens: 40},
|
||||
},
|
||||
ElectionId: 2,
|
||||
VoterIdentity: &voterIdentity,
|
||||
},
|
||||
expectedCode: http.StatusUnprocessableEntity,
|
||||
@ -619,7 +640,7 @@ func TestCreateVotes_UnknownVotersElectionMaxVotersReached(t *testing.T) {
|
||||
On("CountByElection", mock.Anything).
|
||||
Return(10, nil)
|
||||
|
||||
path := "/votes"
|
||||
path := "/election/1/votes"
|
||||
requestBody := api.CreateVotesRequest{
|
||||
Choices: []struct {
|
||||
ChoiceText string `json:"choiceText"`
|
||||
@ -628,7 +649,6 @@ func TestCreateVotes_UnknownVotersElectionMaxVotersReached(t *testing.T) {
|
||||
{ChoiceText: "Gandhi", Tokens: 60},
|
||||
{ChoiceText: "Buddha", Tokens: 40},
|
||||
},
|
||||
ElectionId: 1,
|
||||
VoterIdentity: nil,
|
||||
}
|
||||
requestBodyJson, err := json.Marshal(requestBody)
|
||||
@ -671,7 +691,7 @@ func TestCreateVotes_ExpiredElection(t *testing.T) {
|
||||
On("CountByElection", mock.Anything).
|
||||
Return(10, nil)
|
||||
|
||||
path := "/votes"
|
||||
path := "/election/1/votes"
|
||||
requestBody := api.CreateVotesRequest{
|
||||
Choices: []struct {
|
||||
ChoiceText string `json:"choiceText"`
|
||||
@ -680,7 +700,6 @@ func TestCreateVotes_ExpiredElection(t *testing.T) {
|
||||
{ChoiceText: "Gandhi", Tokens: 60},
|
||||
{ChoiceText: "Buddha", Tokens: 40},
|
||||
},
|
||||
ElectionId: 1,
|
||||
VoterIdentity: nil,
|
||||
}
|
||||
requestBodyJson, err := json.Marshal(requestBody)
|
||||
|
@ -22,11 +22,9 @@ func (app *application) serverError(w http.ResponseWriter, r *http.Request, err
|
||||
func (app *application) clientError(w http.ResponseWriter, status int, message string) {
|
||||
w.WriteHeader(status)
|
||||
var response = api.ErrorResponse{
|
||||
Code: http.StatusUnprocessableEntity,
|
||||
Details: &map[string]interface{}{
|
||||
"error": message,
|
||||
},
|
||||
Message: "There was an error in the request",
|
||||
Code: status,
|
||||
Details: nil,
|
||||
Message: message,
|
||||
}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
@ -38,16 +36,7 @@ func (app *application) unprocessableEntityError(w http.ResponseWriter, v valida
|
||||
Details: &map[string]interface{}{
|
||||
"fields": v.FieldErrors,
|
||||
},
|
||||
Message: "Election data is invalid",
|
||||
}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func (app *application) unprocessableEntityErrorSingle(w http.ResponseWriter, err error) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
var response = api.ErrorResponse{
|
||||
Code: http.StatusUnprocessableEntity,
|
||||
Message: err.Error(),
|
||||
Message: "Request data is invalid",
|
||||
}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
@ -58,6 +58,46 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/election/{id}/votes:
|
||||
post:
|
||||
tags:
|
||||
- vote
|
||||
summary: Cast your votes for an election
|
||||
operationId: createVotes
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: The ID of the election
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateVotesRequest"
|
||||
responses:
|
||||
200:
|
||||
description: Votes cast
|
||||
404:
|
||||
description: Election not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
422:
|
||||
description: Unprocessable content
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
500:
|
||||
description: Server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/election/{id}/results:
|
||||
get:
|
||||
tags:
|
||||
@ -90,33 +130,6 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/votes:
|
||||
post:
|
||||
tags:
|
||||
- vote
|
||||
summary: Cast your votes for an election
|
||||
operationId: createVotes
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateVotesRequest"
|
||||
responses:
|
||||
200:
|
||||
description: Votes cast
|
||||
422:
|
||||
description: Unprocessable content
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
500:
|
||||
description: Server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Election:
|
||||
@ -266,14 +279,11 @@ components:
|
||||
CreateVotesRequest:
|
||||
type: object
|
||||
required:
|
||||
- electionId
|
||||
- choices
|
||||
properties:
|
||||
voterIdentity:
|
||||
type: string
|
||||
description: Must be filled if election has known voters
|
||||
electionId:
|
||||
type: integer
|
||||
choices:
|
||||
type: array
|
||||
items:
|
||||
|
@ -18,7 +18,7 @@ func (app *application) routes() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("POST /election", app.createElection)
|
||||
mux.HandleFunc("POST /votes", app.createVotes)
|
||||
mux.HandleFunc("POST /election/{id}/votes", app.createVotes)
|
||||
|
||||
standard := alice.New(app.recoverPanic, app.logRequest)
|
||||
return standard.Then(mux)
|
||||
|
Reference in New Issue
Block a user