diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go index 7325cb4..e355fde 100644 --- a/cmd/web/handlers.go +++ b/cmd/web/handlers.go @@ -2,6 +2,7 @@ package main import ( api "code.dlmw.ch/dlmw/qv/internal" + "code.dlmw.ch/dlmw/qv/internal/mappers" "code.dlmw.ch/dlmw/qv/internal/models" "code.dlmw.ch/dlmw/qv/internal/validator" "code.dlmw.ch/dlmw/qv/ui" @@ -312,3 +313,23 @@ func getResultsFromVotes(votes *[]models.Vote) []api.VotesForChoice { return results } + +func (app *application) GetElection(w http.ResponseWriter, r *http.Request, id int) { + election, err := app.elections.GetById(id) + if err != nil { + if errors.Is(sql.ErrNoRows, err) { + app.clientError(w, http.StatusNotFound, fmt.Sprintf("couldn't find election with id %v", id)) + return + } + app.serverError(w, r, err) + return + } + + response, err := json.Marshal(mappers.ElectionResponse(election)) + if err != nil { + app.serverError(w, r, err) + return + } + + w.Write(response) +} diff --git a/cmd/web/handlers_test.go b/cmd/web/handlers_test.go index af8937c..242cfac 100644 --- a/cmd/web/handlers_test.go +++ b/cmd/web/handlers_test.go @@ -192,6 +192,50 @@ func TestCreateElection(t *testing.T) { } } +func TestCreateElection_KnownVoters(t *testing.T) { + app := newTestApplication(t) + server := newTestServer(t, app.routes()) + defer server.Close() + + mockElections := app.elections.(*mockElectionModel) + mockElections. + On("Insert", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(1, nil) + + mockVoters := app.voters.(*mockVoterModel) + mockVoters. + On("InsertMultiple", mock.Anything, mock.Anything). + Return([]int{1}, nil) + + path := baseUri + "election" + requestBody := api.CreateElectionRequest{ + Choices: []string{"宮本武蔵", "伊東一刀斎"}, + ExpiresAt: time.Now().Add(24 * time.Hour), + AreVotersKnown: true, + MaxVoters: 100, + Name: "強", + Tokens: 100, + } + requestBodyJson, err := json.Marshal(requestBody) + if err != nil { + t.Fatal(err) + } + + code, _, body := server.post(t, path, bytes.NewReader(requestBodyJson)) + + assert.Equal(t, http.StatusOK, code) + + var voterIdentities struct { + VoterIdentities []string + } + err = json.Unmarshal([]byte(body), &voterIdentities) + if err != nil { + t.Fatal(err) + } + + assert.Len(t, voterIdentities.VoterIdentities, 100) +} + func TestCreateElection_ServerError(t *testing.T) { app := newTestApplication(t) server := newTestServer(t, app.routes()) diff --git a/cmd/web/openapi.yml b/cmd/web/openapi.yml index b8de6e6..4fd0bbd 100644 --- a/cmd/web/openapi.yml +++ b/cmd/web/openapi.yml @@ -22,6 +22,38 @@ tags: description: Retrieve data related to votes paths: + /election/{id}: + get: + tags: + - election + summary: Get an election + operationId: getElection + parameters: + - name: id + in: path + required: true + description: The ID of the election + schema: + type: integer + responses: + 200: + description: Election returned + content: + application/json: + schema: + $ref: "#/components/schemas/Election" + 400: + description: Request malformed + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + 404: + description: Election not found + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /election: post: tags: @@ -77,7 +109,7 @@ paths: schema: $ref: "#/components/schemas/CreateVotesRequest" responses: - 200: + 201: description: Votes cast 400: description: Request malformed @@ -145,6 +177,37 @@ paths: components: schemas: + Election: + type: object + required: + - id + - name + - tokens + - areVotersKnown + - maxVoters + - createdAt + - expiresAt + - choices + properties: + id: + type: integer + name: + type: string + tokens: + type: integer + areVotersKnown: + type: boolean + maxVoters: + type: integer + createdAt: + type: string + expiresAt: + type: string + choices: + type: array + items: + type: string + CreateElectionRequest: type: object required: diff --git a/internal/generated.go b/internal/generated.go index 098df8e..0b38806 100644 --- a/internal/generated.go +++ b/internal/generated.go @@ -41,6 +41,18 @@ type CreateVotesRequest struct { VoterIdentity *string `json:"voterIdentity,omitempty"` } +// Election defines model for Election. +type Election struct { + AreVotersKnown bool `json:"areVotersKnown"` + Choices []string `json:"choices"` + CreatedAt string `json:"createdAt"` + ExpiresAt string `json:"expiresAt"` + Id int `json:"id"` + MaxVoters int `json:"maxVoters"` + Name string `json:"name"` + Tokens int `json:"tokens"` +} + // ElectionResultsResponse defines model for ElectionResultsResponse. type ElectionResultsResponse struct { Results *[]VotesForChoice `json:"results,omitempty"` @@ -75,6 +87,9 @@ type ServerInterface interface { // Create a new election // (POST /election) CreateElection(w http.ResponseWriter, r *http.Request) + // Get an election + // (GET /election/{id}) + GetElection(w http.ResponseWriter, r *http.Request, id int) // Get the results of an election // (GET /election/{id}/results) GetElectionResults(w http.ResponseWriter, r *http.Request, id int) @@ -107,6 +122,32 @@ func (siw *ServerInterfaceWrapper) CreateElection(w http.ResponseWriter, r *http handler.ServeHTTP(w, r.WithContext(ctx)) } +// GetElection operation middleware +func (siw *ServerInterfaceWrapper) GetElection(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "id" ------------- + var id int + + err = runtime.BindStyledParameterWithOptions("simple", "id", r.PathValue("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetElection(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + // GetElectionResults operation middleware func (siw *ServerInterfaceWrapper) GetElectionResults(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -274,6 +315,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H } m.HandleFunc("POST "+options.BaseURL+"/election", wrapper.CreateElection) + m.HandleFunc("GET "+options.BaseURL+"/election/{id}", wrapper.GetElection) m.HandleFunc("GET "+options.BaseURL+"/election/{id}/results", wrapper.GetElectionResults) m.HandleFunc("POST "+options.BaseURL+"/election/{id}/votes", wrapper.CreateVotes) diff --git a/internal/mappers/elections.go b/internal/mappers/elections.go new file mode 100644 index 0000000..4e459e0 --- /dev/null +++ b/internal/mappers/elections.go @@ -0,0 +1,19 @@ +package mappers + +import ( + api "code.dlmw.ch/dlmw/qv/internal" + "code.dlmw.ch/dlmw/qv/internal/models" +) + +func ElectionResponse(election *models.Election) *api.Election { + return &api.Election{ + Id: election.ID, + Name: election.Name, + Tokens: election.Tokens, + AreVotersKnown: election.AreVotersKnown, + MaxVoters: election.MaxVoters, + CreatedAt: election.CreatedAt.String(), + ExpiresAt: election.ExpiresAt.String(), + Choices: election.Choices, + } +} diff --git a/internal/mappers/elections_test.go b/internal/mappers/elections_test.go new file mode 100644 index 0000000..52ff1d1 --- /dev/null +++ b/internal/mappers/elections_test.go @@ -0,0 +1,46 @@ +package mappers + +import ( + "code.dlmw.ch/dlmw/qv/internal/models" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestElectionResponse(t *testing.T) { + layout := "2006-01-02 15:04:05 -0700" + parsedCreatedAtTime, err := time.Parse(layout, "2025-01-14 15:13:37 +0000") + if err != nil { + t.Fatal(err.Error()) + } + + parsedExpiresAtTime, err := time.Parse(layout, "2025-01-15 15:13:37 +0000") + if err != nil { + t.Fatal(err.Error()) + } + + election := models.Election{ + ID: 15, + Name: "The best", + Tokens: 100, + AreVotersKnown: true, + MaxVoters: 150, + CreatedAt: parsedCreatedAtTime.In(time.UTC), + ExpiresAt: parsedExpiresAtTime.In(time.UTC), + Choices: []string{"You", "Me"}, + } + + response := ElectionResponse(&election) + + assert.Equal(t, 15, response.Id) + assert.Equal(t, "The best", response.Name) + assert.Equal(t, 100, response.Tokens) + assert.Equal(t, true, response.AreVotersKnown) + assert.Equal(t, 150, response.MaxVoters) + assert.Equal(t, "2025-01-14 15:13:37 +0000 UTC", response.CreatedAt) + assert.Equal(t, "2025-01-15 15:13:37 +0000 UTC", response.ExpiresAt) + + for _, choice := range election.Choices { + assert.Contains(t, election.Choices, choice) + } +}