From 0229f78976c6f08f5f9a43454382ab383217249d Mon Sep 17 00:00:00 2001 From: dylan Date: Thu, 2 Jan 2025 19:24:32 +0100 Subject: [PATCH] Incomplete implementation of createVotes --- cmd/web/handlers.go | 39 ++++++++++++-- cmd/web/helpers.go | 9 ++++ cmd/web/openapi.yml | 8 ++- cmd/web/routes.go | 2 +- cmd/web/testutils_test.go | 13 +++++ internal/generated.go | 12 +++-- internal/migrations/sql/000001_init.up.sql | 2 +- internal/models/elections.go | 60 ++++++++++++++++++++++ internal/validator/validator.go | 4 ++ 9 files changed, 136 insertions(+), 13 deletions(-) diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go index 402a470..7debd6c 100644 --- a/cmd/web/handlers.go +++ b/cmd/web/handlers.go @@ -3,7 +3,9 @@ package main import ( api "code.dlmw.ch/dlmw/qv/internal" "code.dlmw.ch/dlmw/qv/internal/validator" + "database/sql" "encoding/json" + "errors" "fmt" "math/rand" "net/http" @@ -107,17 +109,16 @@ type createVotesRequestWithValidator struct { } func (r *createVotesRequestWithValidator) isValid() bool { - r.CheckField(validator.NotBlank(*r.VoterIdentity), "voterIdentity", "must not be blank") - r.CheckField(validator.GreaterThan(*r.ElectionId, 0), "electionId", "must be greater than 0") + 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") + for _, choice := range r.Choices { + r.CheckField(validator.NotBlank(choice.ChoiceText), "choiceText", "must not be blank") } return r.Valid() } -func (app *application) createVote(w http.ResponseWriter, r *http.Request) { +func (app *application) createVotes(w http.ResponseWriter, r *http.Request) { var request createVotesRequestWithValidator if err := app.unmarshalRequest(r, &request); err != nil { @@ -129,4 +130,32 @@ func (app *application) createVote(w http.ResponseWriter, r *http.Request) { app.unprocessableEntityError(w, request.Validator) return } + + election, err := app.elections.GetById(request.ElectionId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + app.unprocessableEntityErrorSingle(w, fmt.Errorf("election with id %v doesn't exist", request.ElectionId)) + return + } + app.serverError(w, r, err) + } + + // TODO check if he has voted + + //var voterIdentity string + if election.AreVotersKnown { + if request.VoterIdentity == nil || validator.Blank(*request.VoterIdentity) { + app.unprocessableEntityErrorSingle(w, fmt.Errorf("election has known voters; you must provide an identity provided by the organizer")) + return + } + //voterIdentity = *request.VoterIdentity + } else { + // TODO: get requester's IP address as identity + } + + // TODO verify if choice exists + // TODO count tokens to make sure user isn't trying to cheat + + json, _ := json.Marshal(election) + w.Write(json) } diff --git a/cmd/web/helpers.go b/cmd/web/helpers.go index 1df3466..fb74545 100644 --- a/cmd/web/helpers.go +++ b/cmd/web/helpers.go @@ -43,6 +43,15 @@ func (app *application) unprocessableEntityError(w http.ResponseWriter, v valida 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(), + } + json.NewEncoder(w).Encode(response) +} + func (app *application) unmarshalRequest(r *http.Request, dst any) error { body, err := io.ReadAll(r.Body) if err != nil { diff --git a/cmd/web/openapi.yml b/cmd/web/openapi.yml index c294c3d..66028a7 100644 --- a/cmd/web/openapi.yml +++ b/cmd/web/openapi.yml @@ -221,15 +221,21 @@ components: CreateVotesRequest: type: object + required: + - electionId + - choices properties: voterIdentity: type: string - minLength: 1 + description: Must be filled if election has known voters electionId: type: integer choices: type: array items: + required: + - choiceText + - tokens properties: choiceText: type: string diff --git a/cmd/web/routes.go b/cmd/web/routes.go index fa75899..732e022 100644 --- a/cmd/web/routes.go +++ b/cmd/web/routes.go @@ -17,7 +17,7 @@ func (app *application) routes() http.Handler { mux := http.NewServeMux() mux.HandleFunc("POST /election", app.createElection) - mux.HandleFunc("POST /vote", app.createVote) + mux.HandleFunc("POST /votes", app.createVotes) standard := alice.New(app.recoverPanic, app.logRequest) return standard.Then(mux) diff --git a/cmd/web/testutils_test.go b/cmd/web/testutils_test.go index f22ac42..7e9e228 100644 --- a/cmd/web/testutils_test.go +++ b/cmd/web/testutils_test.go @@ -1,6 +1,7 @@ package main import ( + "code.dlmw.ch/dlmw/qv/internal/models" "io" "log/slog" "net/http" @@ -48,6 +49,18 @@ func (e *mockElectionModel) Insert(name string, tokens int, areVotersKnown bool, return 1, nil } +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().String(), + ExpiresAt: time.Now().Add(100 * time.Hour).String(), + }, nil +} + type mockVoterModel struct { } diff --git a/internal/generated.go b/internal/generated.go index c3f668f..7c6196f 100644 --- a/internal/generated.go +++ b/internal/generated.go @@ -30,11 +30,13 @@ type CreateElectionResponse struct { // CreateVotesRequest defines model for CreateVotesRequest. type CreateVotesRequest struct { - Choices *[]struct { - ChoiceText *string `json:"choiceText,omitempty"` - Tokens *int `json:"tokens,omitempty"` - } `json:"choices,omitempty"` - ElectionId *int `json:"electionId,omitempty"` + Choices []struct { + ChoiceText string `json:"choiceText"` + Tokens int `json:"tokens"` + } `json:"choices"` + ElectionId int `json:"electionId"` + + // VoterIdentity Must be filled if election has known voters VoterIdentity *string `json:"voterIdentity,omitempty"` } diff --git a/internal/migrations/sql/000001_init.up.sql b/internal/migrations/sql/000001_init.up.sql index 754c3c8..70f18a0 100644 --- a/internal/migrations/sql/000001_init.up.sql +++ b/internal/migrations/sql/000001_init.up.sql @@ -3,7 +3,7 @@ CREATE TABLE elections ( name TEXT NOT NULL, tokens INTEGER NOT NULL, are_voters_known INTEGER NOT NULL, - max_voters INTEGER, -- mandatory when voters are known + max_voters INTEGER NOT NULL, -- must be greater than 0 when voters are known created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME NOT NULL, CHECK (are_voters_known = 0 OR (are_voters_known = 1 AND max_voters IS NOT NULL AND max_voters >= 1)) diff --git a/internal/models/elections.go b/internal/models/elections.go index d68e451..64773f4 100644 --- a/internal/models/elections.go +++ b/internal/models/elections.go @@ -2,17 +2,30 @@ package models import ( "database/sql" + "fmt" "time" ) type ElectionModelInterface interface { Insert(name string, tokens int, areVotersKnown bool, maxVoters int, Choices []string, ExpiresAt time.Time) (int, error) + GetById(id int) (*Election, error) } type ElectionModel struct { DB *sql.DB } +type Election struct { + ID int + Name string + Tokens int + AreVotersKnown bool + MaxVoters int + CreatedAt string + ExpiresAt string + Choices []string +} + func (e *ElectionModel) Insert(name string, tokens int, areVotersKnown bool, maxVoters int, choices []string, expiresAt time.Time) (int, error) { tx, err := e.DB.Begin() if err != nil { @@ -53,3 +66,50 @@ func (e *ElectionModel) Insert(name string, tokens int, areVotersKnown bool, max return int(electionID), nil } + +func (e *ElectionModel) GetById(id int) (*Election, error) { + query := ` + SELECT id, name, tokens, are_voters_known, max_voters, created_at, expires_at + FROM elections + WHERE id = ? + ` + + row := e.DB.QueryRow(query, id) + + election := &Election{} + var areVotersKnown int + + err := row.Scan(&election.ID, &election.Name, &election.Tokens, &areVotersKnown, &election.MaxVoters, &election.CreatedAt, &election.ExpiresAt) + if err != nil { + return nil, err + } + + election.AreVotersKnown = areVotersKnown == 1 + + // Retrieve choices for the election + queryChoices := ` + SELECT text + FROM choices + WHERE election_id = ? + ` + + rows, err := e.DB.Query(queryChoices, id) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var choice string + if err := rows.Scan(&choice); err != nil { + return nil, err + } + election.Choices = append(election.Choices, choice) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating over choices for election ID %d: %v", id, err) + } + + return election, nil +} diff --git a/internal/validator/validator.go b/internal/validator/validator.go index e609409..2965fd2 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -36,6 +36,10 @@ func (v *Validator) CheckField(ok bool, key, message string) { } } +func Blank(value string) bool { + return strings.TrimSpace(value) == "" +} + func NotBlank(value string) bool { return strings.TrimSpace(value) != "" }