Insert election with uuid instead of auto-generated id

This commit is contained in:
2025-01-20 10:04:15 +01:00
parent 729fbecae6
commit 5668b1cd6a
13 changed files with 94 additions and 360 deletions

View File

@ -145,7 +145,7 @@ func (r *createVotesRequestWithValidator) isValid() bool {
return r.Valid() return r.Valid()
} }
func (app *application) CreateVotes(w http.ResponseWriter, r *http.Request, id int) { func (app *application) CreateVotes(w http.ResponseWriter, r *http.Request, id string) {
var request createVotesRequestWithValidator var request createVotesRequestWithValidator
if err := app.unmarshalRequest(r, &request); err != nil { if err := app.unmarshalRequest(r, &request); err != nil {
@ -284,7 +284,7 @@ func (app *application) createVotesHandleUnknownVotersElection(w http.ResponseWr
return voterIdentity, nil return voterIdentity, nil
} }
func (app *application) GetElectionResults(w http.ResponseWriter, r *http.Request, id int) { func (app *application) GetElectionResults(w http.ResponseWriter, r *http.Request, id string) {
votes, err := app.votes.GetByElection(id) votes, err := app.votes.GetByElection(id)
if err != nil { if err != nil {
if errors.Is(sql.ErrNoRows, err) { if errors.Is(sql.ErrNoRows, err) {
@ -325,7 +325,7 @@ func getResultsFromVotes(votes *[]models.Vote) []api.VotesForChoice {
return results return results
} }
func (app *application) GetElection(w http.ResponseWriter, r *http.Request, id int) { func (app *application) GetElection(w http.ResponseWriter, r *http.Request, id string) {
election, err := app.elections.GetById(id) election, err := app.elections.GetById(id)
if err != nil { if err != nil {
if errors.Is(sql.ErrNoRows, err) { if errors.Is(sql.ErrNoRows, err) {

View File

@ -7,6 +7,7 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"net/http" "net/http"
@ -21,10 +22,11 @@ func TestCreateElection(t *testing.T) {
server := newTestServer(t, app.routes()) server := newTestServer(t, app.routes())
defer server.Close() defer server.Close()
id, _ := uuid.NewV7()
mockElections := app.elections.(*mockElectionModel) mockElections := app.elections.(*mockElectionModel)
mockElections. mockElections.
On("Insert", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). On("Insert", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(1, nil) Return(id.String(), nil)
path := baseUri + "election" path := baseUri + "election"
@ -197,10 +199,11 @@ func TestCreateElection_KnownVoters(t *testing.T) {
server := newTestServer(t, app.routes()) server := newTestServer(t, app.routes())
defer server.Close() defer server.Close()
electionId, _ := uuid.NewV7()
mockElections := app.elections.(*mockElectionModel) mockElections := app.elections.(*mockElectionModel)
mockElections. mockElections.
On("Insert", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). On("Insert", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(1, nil) Return(electionId.String(), nil)
mockVoters := app.voters.(*mockVoterModel) mockVoters := app.voters.(*mockVoterModel)
mockVoters. mockVoters.
@ -270,11 +273,12 @@ func TestCreateVotes_UnknownVotersElection(t *testing.T) {
server := newTestServer(t, app.routes()) server := newTestServer(t, app.routes())
defer server.Close() defer server.Close()
id, _ := uuid.NewV7()
mockElections := app.elections.(*mockElectionModel) mockElections := app.elections.(*mockElectionModel)
mockElections. mockElections.
On("GetById", mock.Anything). On("GetById", mock.Anything).
Return(&models.Election{ Return(&models.Election{
ID: 1, ID: id.String(),
Name: "Guy of the year", Name: "Guy of the year",
Tokens: 100, Tokens: 100,
AreVotersKnown: false, AreVotersKnown: false,
@ -373,11 +377,12 @@ func TestCreateVotes_KnownVotersElection(t *testing.T) {
server := newTestServer(t, app.routes()) server := newTestServer(t, app.routes())
defer server.Close() defer server.Close()
id, _ := uuid.NewV7()
mockElections := app.elections.(*mockElectionModel) mockElections := app.elections.(*mockElectionModel)
mockElections. mockElections.
On("GetById", mock.Anything). On("GetById", mock.Anything).
Return(&models.Election{ Return(&models.Election{
ID: 1, ID: id.String(),
Name: "Guy of the year", Name: "Guy of the year",
Tokens: 100, Tokens: 100,
AreVotersKnown: true, AreVotersKnown: true,
@ -534,7 +539,7 @@ func TestCreateVotes_NonExistingElection(t *testing.T) {
assert.Equal(t, http.StatusNotFound, code) assert.Equal(t, http.StatusNotFound, code)
} }
func TestCreateVotes_NonNumberElectionID(t *testing.T) { func TestCreateVotes_NonUuidElectionID(t *testing.T) {
app := newTestApplication(t) app := newTestApplication(t)
server := newTestServer(t, app.routes()) server := newTestServer(t, app.routes())
defer server.Close() defer server.Close()
@ -562,7 +567,7 @@ func TestCreateVotes_NonNumberElectionID(t *testing.T) {
code, _, _ := server.post(t, path, bytes.NewReader(requestBodyJson)) code, _, _ := server.post(t, path, bytes.NewReader(requestBodyJson))
assert.Equal(t, http.StatusBadRequest, code) assert.Equal(t, http.StatusNotFound, code)
} }
func TestCreateVotes_AlreadyVoted(t *testing.T) { func TestCreateVotes_AlreadyVoted(t *testing.T) {
@ -570,8 +575,9 @@ func TestCreateVotes_AlreadyVoted(t *testing.T) {
server := newTestServer(t, app.routes()) server := newTestServer(t, app.routes())
defer server.Close() defer server.Close()
unknownVotersElectionId, _ := uuid.NewV7()
unknownVotersElection := models.Election{ unknownVotersElection := models.Election{
ID: 1, ID: unknownVotersElectionId.String(),
Name: "Guy of the year", Name: "Guy of the year",
Tokens: 100, Tokens: 100,
AreVotersKnown: false, AreVotersKnown: false,
@ -580,15 +586,18 @@ func TestCreateVotes_AlreadyVoted(t *testing.T) {
ExpiresAt: time.Now().Add(24 * time.Hour), ExpiresAt: time.Now().Add(24 * time.Hour),
Choices: []string{"Gandhi", "Buddha"}, Choices: []string{"Gandhi", "Buddha"},
} }
knownVotersElection := unknownVotersElection knownVotersElection := unknownVotersElection
knownVotersElectionId, _ := uuid.NewV7()
knownVotersElection.ID = knownVotersElectionId.String()
knownVotersElection.AreVotersKnown = true knownVotersElection.AreVotersKnown = true
mockElections := app.elections.(*mockElectionModel) mockElections := app.elections.(*mockElectionModel)
mockElections. mockElections.
On("GetById", 1). On("GetById", knownVotersElectionId.String()).
Return(&knownVotersElection, nil) Return(&knownVotersElection, nil)
mockElections. mockElections.
On("GetById", 2). On("GetById", unknownVotersElectionId.String()).
Return(&unknownVotersElection, nil) Return(&unknownVotersElection, nil)
mockVotes := app.votes.(*mockVoteModel) mockVotes := app.votes.(*mockVoteModel)
@ -601,8 +610,9 @@ func TestCreateVotes_AlreadyVoted(t *testing.T) {
On("Exists", mock.Anything, mock.Anything). On("Exists", mock.Anything, mock.Anything).
Return(true, nil) Return(true, nil)
knownVotersElectionPath := baseUri + "election/1/votes" layout := baseUri + "election/%v/votes"
unknownVotersElectionPath := baseUri + "election/2/votes" knownVotersElectionPath := fmt.Sprintf(layout, unknownVotersElectionId)
unknownVotersElectionPath := fmt.Sprintf(layout, knownVotersElectionId)
voterIdentity := "anything" voterIdentity := "anything"
tests := []struct { tests := []struct {
@ -661,11 +671,12 @@ func TestCreateVotes_UnknownVotersElectionMaxVotersReached(t *testing.T) {
server := newTestServer(t, app.routes()) server := newTestServer(t, app.routes())
defer server.Close() defer server.Close()
id, _ := uuid.NewV7()
mockElections := app.elections.(*mockElectionModel) mockElections := app.elections.(*mockElectionModel)
mockElections. mockElections.
On("GetById", mock.Anything). On("GetById", mock.Anything).
Return(&models.Election{ Return(&models.Election{
ID: 1, ID: id.String(),
Name: "Guy of the year", Name: "Guy of the year",
Tokens: 100, Tokens: 100,
AreVotersKnown: false, AreVotersKnown: false,
@ -712,11 +723,12 @@ func TestCreateVotes_ExpiredElection(t *testing.T) {
server := newTestServer(t, app.routes()) server := newTestServer(t, app.routes())
defer server.Close() defer server.Close()
id, _ := uuid.NewV7()
mockElections := app.elections.(*mockElectionModel) mockElections := app.elections.(*mockElectionModel)
mockElections. mockElections.
On("GetById", mock.Anything). On("GetById", mock.Anything).
Return(&models.Election{ Return(&models.Election{
ID: 1, ID: id.String(),
Name: "Guy of the year", Name: "Guy of the year",
Tokens: 100, Tokens: 100,
AreVotersKnown: false, AreVotersKnown: false,
@ -763,38 +775,39 @@ func TestGetElectionResults(t *testing.T) {
server := newTestServer(t, app.routes()) server := newTestServer(t, app.routes())
defer server.Close() defer server.Close()
electionID, _ := uuid.NewV7()
votes := []models.Vote{ votes := []models.Vote{
{ {
VoterIdentity: "Voter1", VoterIdentity: "Voter1",
ElectionID: 1, ElectionID: electionID.String(),
ChoiceText: "Choice1", ChoiceText: "Choice1",
Tokens: 2, Tokens: 2,
CreatedAt: time.Now(), CreatedAt: time.Now(),
}, },
{ {
VoterIdentity: "Voter2", VoterIdentity: "Voter2",
ElectionID: 1, ElectionID: electionID.String(),
ChoiceText: "Choice2", ChoiceText: "Choice2",
Tokens: 4, Tokens: 4,
CreatedAt: time.Now(), CreatedAt: time.Now(),
}, },
{ {
VoterIdentity: "Voter3", VoterIdentity: "Voter3",
ElectionID: 1, ElectionID: electionID.String(),
ChoiceText: "Choice3", ChoiceText: "Choice3",
Tokens: 6, Tokens: 6,
CreatedAt: time.Now(), CreatedAt: time.Now(),
}, },
{ {
VoterIdentity: "Voter4", VoterIdentity: "Voter4",
ElectionID: 1, ElectionID: electionID.String(),
ChoiceText: "Choice1", ChoiceText: "Choice1",
Tokens: 8, Tokens: 8,
CreatedAt: time.Now(), CreatedAt: time.Now(),
}, },
{ {
VoterIdentity: "Voter5", VoterIdentity: "Voter5",
ElectionID: 1, ElectionID: electionID.String(),
ChoiceText: "Choice2", ChoiceText: "Choice2",
Tokens: 10, Tokens: 10,
CreatedAt: time.Now(), CreatedAt: time.Now(),
@ -802,10 +815,10 @@ func TestGetElectionResults(t *testing.T) {
} }
mockVotes := app.votes.(*mockVoteModel) mockVotes := app.votes.(*mockVoteModel)
mockVotes. mockVotes.
On("GetByElection", mock.Anything). On("GetByElection", electionID.String()).
Return(&votes, nil) Return(&votes, nil)
path := baseUri + "election/1/results" path := baseUri + fmt.Sprintf("election/%v/results", electionID)
code, _, body := server.get(t, path) code, _, body := server.get(t, path)
assert.Equal(t, http.StatusOK, code) assert.Equal(t, http.StatusOK, code)
@ -857,11 +870,12 @@ func TestGetElection_Found(t *testing.T) {
server := newTestServer(t, app.routes()) server := newTestServer(t, app.routes())
defer server.Close() defer server.Close()
id, _ := uuid.NewV7()
mockElections := app.elections.(*mockElectionModel) mockElections := app.elections.(*mockElectionModel)
mockElections. mockElections.
On("GetById", mock.Anything). On("GetById", mock.Anything).
Return(&models.Election{ Return(&models.Election{
ID: 1, ID: id.String(),
Name: "Guy of the year", Name: "Guy of the year",
Tokens: 100, Tokens: 100,
AreVotersKnown: false, AreVotersKnown: false,

View File

@ -1,5 +1,3 @@
//go:build !integration
//go:generate oapi-codegen --config=oapi-codegen.yml openapi.yml //go:generate oapi-codegen --config=oapi-codegen.yml openapi.yml
package main package main

View File

@ -1,281 +0,0 @@
//go:build integration
package main
import (
"code.dlmw.ch/dlmw/qv/internal/migrations"
"code.dlmw.ch/dlmw/qv/internal/models"
"context"
"database/sql"
"errors"
"fmt"
. "github.com/Eun/go-hit"
"log/slog"
"net/http"
"os"
"strings"
"time"
)
var addr = ":8080"
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
err := os.Remove("./qv.integration.sqlite")
if err != nil {
logger.Error("couldn't delete qv.integration.sqlite")
}
db, err := openDb()
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
defer db.Close()
err = migrations.Run(db)
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
app := &application{
logger: logger,
elections: &models.ElectionModel{DB: db},
voters: &models.VoterModel{DB: db},
votes: &models.VoteModel{DB: db},
}
logger.Info("Starting integration tests server", "addr", addr)
srv := &http.Server{
Addr: addr,
Handler: app.routes(),
ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
IdleTimeout: time.Minute,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
go func() {
logger.Info("Starting integration test server", "addr", addr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Error("Server error", "error", err)
os.Exit(1)
}
}()
time.Sleep(1 * time.Second) // wait until srv starts
runTests()
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
srv.Shutdown(ctx)
}
func openDb() (*sql.DB, error) {
db, err := sql.Open("sqlite3", "./qv.integration.sqlite?_foreign_keys=on")
if err == nil {
err = db.Ping()
}
return db, err
}
func runTests() {
runCreateElectionTests()
runCreateVotesTests()
}
func runCreateElectionTests() {
template := CombineSteps(
Post("http://127.0.0.1:8080/api/election"),
)
MustDo(
template,
Send().Body().String(`
{
"name": "Guy of the year",
"tokens": 100,
"areVotersKnown": false,
"maxVoters": 10,
"expiresAt": "2124-12-31T14:15:22Z",
"choices": [
"Gandhi",
"Buddha"
]
}`),
Expect().Status().Equal(http.StatusOK),
Expect().Headers("Location").Contains("/election/1"),
)
MustDo(
template,
Send().Body().String(`
{
"name": "Guy of the year",
"tokens": 100,
"areVotersKnown": false,
"maxVoters": 0,
"expiresAt": "2124-12-31T14:15:22Z",
"choices": [
"Gandhi",
"Buddha"
]
}`),
Expect().Status().Equal(http.StatusOK),
Expect().Headers("Location").Contains("/election/2"),
)
MustDo(
template,
Send().Body().String(`
{
"name": "Guy of the year",
"tokens": 100,
"areVotersKnown": true,
"maxVoters": 10,
"expiresAt": "2124-12-31T14:15:22Z",
"choices": [
"Gandhi",
"Buddha"
]
}`),
Expect().Status().Equal(http.StatusOK),
Expect().Headers("Location").Contains("/election/3"),
Expect().Body().JSON().JQ(".voterIdentities").Len().Equal(10),
)
MustDo(
template,
Send().Body().String(`
{
"name": "Guy of the year",
"tokens": 100,
"areVotersKnown": true,
"maxVoters": -1,
"expiresAt": "2124-12-31T14:15:22Z",
"choices": [
"Gandhi",
"Buddha"
]
}`),
Expect().Status().Equal(http.StatusUnprocessableEntity),
)
MustDo(
template,
Send().Body().String(`
{
"name": "Guy of the year",
"tokens": 100,
"areVotersKnown": false,
"maxVoters": -1,
"expiresAt": "2124-12-31T14:15:22Z",
"choices": [
"Gandhi"
]
}`),
Expect().Status().Equal(http.StatusUnprocessableEntity),
)
MustDo(
template,
Send().Body().String(`
{
"name": "Guy of the year",
"tokens": 100,
"areVotersKnown": false,
"maxVoters": 10,
"expiresAt": "2018-12-31T14:15:22Z",
"choices": [
"Gandhi",
"Buddha"
]
}`),
Expect().Status().Equal(http.StatusUnprocessableEntity),
)
}
func runCreateVotesTests() {
uri := "http://127.0.0.1:8080/api/election/%v/votes"
MustDo(
Post(fmt.Sprintf(uri, "1")),
Send().Body().String(`
{
"choices": [
{
"choiceText": "Buddha",
"tokens": 90
},
{
"choiceText": "Gandhi",
"tokens": 10
}
]
}`),
Expect().Status().Equal(http.StatusCreated),
)
MustDo(
Post(fmt.Sprintf(uri, "3")),
Send().Body().String(`
{
"choices": [
{
"choiceText": "Buddha",
"tokens": 90
},
{
"choiceText": "Gandhi",
"tokens": 10
}
]
}`),
Expect().Status().Equal(http.StatusUnprocessableEntity),
)
voterIdentities := make([]string, 0)
locationHeader := ""
MustDo(
Post("http://127.0.0.1:8080/api/election"),
Send().Body().String(`
{
"name": "Guy of the year",
"tokens": 100,
"areVotersKnown": true,
"maxVoters": 10,
"expiresAt": "2124-12-31T14:15:22Z",
"choices": [
"Gandhi",
"Buddha"
]
}`),
Expect().Status().Equal(http.StatusOK),
Store().Response().Body().JSON().JQ(".voterIdentities").In(&voterIdentities),
Store().Response().Headers("Location").In(&locationHeader),
)
electionId := strings.Split(locationHeader, "/")[2]
MustDo(
Post(fmt.Sprintf(uri, electionId)),
Send().Body().String(fmt.Sprintf(`
{
"voterIdentity": "%v",
"choices": [
{
"choiceText": "Buddha",
"tokens": 90
},
{
"choiceText": "Gandhi",
"tokens": 10
}
]
}`, voterIdentities[0])),
Expect().Status().Equal(http.StatusCreated),
)
}

View File

@ -34,7 +34,7 @@ paths:
required: true required: true
description: The ID of the election description: The ID of the election
schema: schema:
type: integer type: string
responses: responses:
200: 200:
description: Election returned description: Election returned
@ -102,7 +102,7 @@ paths:
required: true required: true
description: The ID of the election description: The ID of the election
schema: schema:
type: integer type: string
requestBody: requestBody:
content: content:
application/json: application/json:
@ -148,7 +148,7 @@ paths:
required: true required: true
description: The ID of the election description: The ID of the election
schema: schema:
type: integer type: string
responses: responses:
200: 200:
description: Election results returned description: Election results returned
@ -190,7 +190,7 @@ components:
- choices - choices
properties: properties:
id: id:
type: integer type: string
name: name:
type: string type: string
tokens: tokens:

View File

@ -65,12 +65,12 @@ type mockElectionModel struct {
mock.Mock mock.Mock
} }
func (e *mockElectionModel) Insert(name string, tokens int, areVotersKnown bool, maxVoters int, choices []string, expiresAt time.Time) (int, error) { func (e *mockElectionModel) Insert(name string, tokens int, areVotersKnown bool, maxVoters int, choices []string, expiresAt time.Time) (string, error) {
args := e.Called(name, tokens, areVotersKnown, maxVoters, choices, expiresAt) args := e.Called(name, tokens, areVotersKnown, maxVoters, choices, expiresAt)
return args.Int(0), args.Error(1) return args.String(0), args.Error(1)
} }
func (e *mockElectionModel) GetById(id int) (*models.Election, error) { func (e *mockElectionModel) GetById(id string) (*models.Election, error) {
args := e.Called(id) args := e.Called(id)
return args.Get(0).(*models.Election), args.Error(1) return args.Get(0).(*models.Election), args.Error(1)
} }
@ -79,17 +79,17 @@ type mockVoterModel struct {
mock.Mock mock.Mock
} }
func (v *mockVoterModel) InsertMultiple(identities []string, electionID int) ([]int, error) { func (v *mockVoterModel) InsertMultiple(identities []string, electionID string) ([]int, error) {
args := v.Called(identities, electionID) args := v.Called(identities, electionID)
return args.Get(0).([]int), args.Error(1) return args.Get(0).([]int), args.Error(1)
} }
func (v *mockVoterModel) CountByElection(electionID int) (int, error) { func (v *mockVoterModel) CountByElection(electionID string) (int, error) {
args := v.Called(electionID) args := v.Called(electionID)
return args.Int(0), args.Error(1) return args.Int(0), args.Error(1)
} }
func (v *mockVoterModel) Exists(voterIdentity string, electionID int) (bool, error) { func (v *mockVoterModel) Exists(voterIdentity string, electionID string) (bool, error) {
args := v.Called(voterIdentity, electionID) args := v.Called(voterIdentity, electionID)
return args.Bool(0), args.Error(1) return args.Bool(0), args.Error(1)
} }
@ -98,17 +98,17 @@ type mockVoteModel struct {
mock.Mock mock.Mock
} }
func (v *mockVoteModel) Insert(voterIdentity string, electionId int, choiceText string, tokens int) (int, error) { func (v *mockVoteModel) Insert(voterIdentity string, electionId string, choiceText string, tokens int) (int, error) {
args := v.Called(voterIdentity, electionId, choiceText, tokens) args := v.Called(voterIdentity, electionId, choiceText, tokens)
return args.Int(0), args.Error(1) return args.Int(0), args.Error(1)
} }
func (v *mockVoteModel) Exists(voterIdentity string, electionID int) (bool, error) { func (v *mockVoteModel) Exists(voterIdentity string, electionID string) (bool, error) {
args := v.Called(voterIdentity, electionID) args := v.Called(voterIdentity, electionID)
return args.Bool(0), args.Error(1) return args.Bool(0), args.Error(1)
} }
func (v *mockVoteModel) GetByElection(electionID int) (*[]models.Vote, error) { func (v *mockVoteModel) GetByElection(electionID string) (*[]models.Vote, error) {
args := v.Called(electionID) args := v.Called(electionID)
return args.Get(0).(*[]models.Vote), args.Error(1) return args.Get(0).(*[]models.Vote), args.Error(1)
} }

View File

@ -47,7 +47,7 @@ type Election struct {
Choices []string `json:"choices"` Choices []string `json:"choices"`
CreatedAt string `json:"createdAt"` CreatedAt string `json:"createdAt"`
ExpiresAt string `json:"expiresAt"` ExpiresAt string `json:"expiresAt"`
Id int `json:"id"` Id string `json:"id"`
MaxVoters int `json:"maxVoters"` MaxVoters int `json:"maxVoters"`
Name string `json:"name"` Name string `json:"name"`
Tokens int `json:"tokens"` Tokens int `json:"tokens"`
@ -89,13 +89,13 @@ type ServerInterface interface {
CreateElection(w http.ResponseWriter, r *http.Request) CreateElection(w http.ResponseWriter, r *http.Request)
// Get an election // Get an election
// (GET /election/{id}) // (GET /election/{id})
GetElection(w http.ResponseWriter, r *http.Request, id int) GetElection(w http.ResponseWriter, r *http.Request, id string)
// Get the results of an election // Get the results of an election
// (GET /election/{id}/results) // (GET /election/{id}/results)
GetElectionResults(w http.ResponseWriter, r *http.Request, id int) GetElectionResults(w http.ResponseWriter, r *http.Request, id string)
// Cast your votes for an election // Cast your votes for an election
// (POST /election/{id}/votes) // (POST /election/{id}/votes)
CreateVotes(w http.ResponseWriter, r *http.Request, id int) CreateVotes(w http.ResponseWriter, r *http.Request, id string)
} }
// ServerInterfaceWrapper converts contexts to parameters. // ServerInterfaceWrapper converts contexts to parameters.
@ -129,7 +129,7 @@ func (siw *ServerInterfaceWrapper) GetElection(w http.ResponseWriter, r *http.Re
var err error var err error
// ------------- Path parameter "id" ------------- // ------------- Path parameter "id" -------------
var id int var id string
err = runtime.BindStyledParameterWithOptions("simple", "id", r.PathValue("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) err = runtime.BindStyledParameterWithOptions("simple", "id", r.PathValue("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil { if err != nil {
@ -155,7 +155,7 @@ func (siw *ServerInterfaceWrapper) GetElectionResults(w http.ResponseWriter, r *
var err error var err error
// ------------- Path parameter "id" ------------- // ------------- Path parameter "id" -------------
var id int var id string
err = runtime.BindStyledParameterWithOptions("simple", "id", r.PathValue("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) err = runtime.BindStyledParameterWithOptions("simple", "id", r.PathValue("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil { if err != nil {
@ -181,7 +181,7 @@ func (siw *ServerInterfaceWrapper) CreateVotes(w http.ResponseWriter, r *http.Re
var err error var err error
// ------------- Path parameter "id" ------------- // ------------- Path parameter "id" -------------
var id int var id string
err = runtime.BindStyledParameterWithOptions("simple", "id", r.PathValue("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) err = runtime.BindStyledParameterWithOptions("simple", "id", r.PathValue("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil { if err != nil {

View File

@ -2,6 +2,7 @@ package mappers
import ( import (
"code.dlmw.ch/dlmw/qv/internal/models" "code.dlmw.ch/dlmw/qv/internal/models"
uuid2 "github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"testing" "testing"
"time" "time"
@ -19,8 +20,9 @@ func TestElectionResponse(t *testing.T) {
t.Fatal(err.Error()) t.Fatal(err.Error())
} }
uuid, _ := uuid2.NewV7()
election := models.Election{ election := models.Election{
ID: 15, ID: uuid.String(),
Name: "The best", Name: "The best",
Tokens: 100, Tokens: 100,
AreVotersKnown: true, AreVotersKnown: true,
@ -32,7 +34,7 @@ func TestElectionResponse(t *testing.T) {
response := ElectionResponse(&election) response := ElectionResponse(&election)
assert.Equal(t, 15, response.Id) assert.Equal(t, uuid.String(), response.Id)
assert.Equal(t, "The best", response.Name) assert.Equal(t, "The best", response.Name)
assert.Equal(t, 100, response.Tokens) assert.Equal(t, 100, response.Tokens)
assert.Equal(t, true, response.AreVotersKnown) assert.Equal(t, true, response.AreVotersKnown)

View File

@ -1,5 +1,5 @@
CREATE TABLE elections ( CREATE TABLE elections (
id INTEGER PRIMARY KEY AUTOINCREMENT, -- TODO: try to generate a UUID id TEXT PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
tokens INTEGER NOT NULL, tokens INTEGER NOT NULL,
are_voters_known INTEGER NOT NULL, are_voters_known INTEGER NOT NULL,

View File

@ -2,12 +2,13 @@ package models
import ( import (
"database/sql" "database/sql"
"github.com/google/uuid"
"time" "time"
) )
type ElectionModelInterface interface { type ElectionModelInterface interface {
Insert(name string, tokens int, areVotersKnown bool, maxVoters int, Choices []string, ExpiresAt time.Time) (int, error) Insert(name string, tokens int, areVotersKnown bool, maxVoters int, Choices []string, ExpiresAt time.Time) (string, error)
GetById(id int) (*Election, error) GetById(id string) (*Election, error)
} }
type ElectionModel struct { type ElectionModel struct {
@ -15,7 +16,7 @@ type ElectionModel struct {
} }
type Election struct { type Election struct {
ID int ID string
Name string Name string
Tokens int Tokens int
AreVotersKnown bool AreVotersKnown bool
@ -25,48 +26,48 @@ type Election struct {
Choices []string Choices []string
} }
func (e *ElectionModel) Insert(name string, tokens int, areVotersKnown bool, maxVoters int, choices []string, expiresAt time.Time) (int, error) { func (e *ElectionModel) Insert(name string, tokens int, areVotersKnown bool, maxVoters int, choices []string, expiresAt time.Time) (string, error) {
tx, err := e.DB.Begin() tx, err := e.DB.Begin()
if err != nil { if err != nil {
return 0, err return "", err
} }
defer tx.Rollback() defer tx.Rollback()
result, err := tx.Exec(` electionID, err := uuid.NewV7()
INSERT INTO elections (name, tokens, are_voters_known, max_voters, expires_at)
VALUES (?, ?, ?, ?, ?)`, name, tokens, areVotersKnown, maxVoters, expiresAt)
if err != nil { if err != nil {
return 0, err return "", err
} }
electionID, err := result.LastInsertId() _, err = tx.Exec(`
INSERT INTO elections (id, name, tokens, are_voters_known, max_voters, expires_at)
VALUES (?, ?, ?, ?, ?, ?)`, electionID, name, tokens, areVotersKnown, maxVoters, expiresAt)
if err != nil { if err != nil {
return 0, err return "", err
} }
stmt, err := tx.Prepare(` stmt, err := tx.Prepare(`
INSERT INTO choices (text, election_id) INSERT INTO choices (text, election_id)
VALUES (?, ?)`) VALUES (?, ?)`)
if err != nil { if err != nil {
return 0, err return "", err
} }
defer stmt.Close() defer stmt.Close()
for _, choice := range choices { for _, choice := range choices {
_, err = stmt.Exec(choice, electionID) _, err = stmt.Exec(choice, electionID)
if err != nil { if err != nil {
return 0, err return "", err
} }
} }
if err = tx.Commit(); err != nil { if err = tx.Commit(); err != nil {
return 0, err return "0", err
} }
return int(electionID), nil return electionID.String(), nil
} }
func (e *ElectionModel) GetById(id int) (*Election, error) { func (e *ElectionModel) GetById(id string) (*Election, error) {
query := ` query := `
SELECT id, name, tokens, are_voters_known, max_voters, created_at, expires_at SELECT id, name, tokens, are_voters_known, max_voters, created_at, expires_at
FROM elections FROM elections

View File

@ -6,16 +6,16 @@ import (
) )
type VoterModelInterface interface { type VoterModelInterface interface {
InsertMultiple(identities []string, electionID int) ([]int, error) InsertMultiple(identities []string, electionID string) ([]int, error)
CountByElection(electionID int) (int, error) CountByElection(electionID string) (int, error)
Exists(voterIdentity string, electionID int) (bool, error) Exists(voterIdentity string, electionID string) (bool, error)
} }
type VoterModel struct { type VoterModel struct {
DB *sql.DB DB *sql.DB
} }
func (v *VoterModel) InsertMultiple(identities []string, electionID int) ([]int, error) { func (v *VoterModel) InsertMultiple(identities []string, electionID string) ([]int, error) {
tx, err := v.DB.Begin() tx, err := v.DB.Begin()
if err != nil { if err != nil {
return nil, err return nil, err
@ -53,7 +53,7 @@ func (v *VoterModel) InsertMultiple(identities []string, electionID int) ([]int,
return voterIDs, nil return voterIDs, nil
} }
func (v *VoterModel) CountByElection(electionID int) (int, error) { func (v *VoterModel) CountByElection(electionID string) (int, error) {
// use a transaction to prevent race conditions // use a transaction to prevent race conditions
tx, err := v.DB.Begin() tx, err := v.DB.Begin()
if err != nil { if err != nil {
@ -82,7 +82,7 @@ func (v *VoterModel) CountByElection(electionID int) (int, error) {
return voterCount, nil return voterCount, nil
} }
func (v *VoterModel) Exists(voterIdentity string, electionID int) (bool, error) { func (v *VoterModel) Exists(voterIdentity string, electionID string) (bool, error) {
query := ` query := `
SELECT EXISTS ( SELECT EXISTS (
SELECT 1 SELECT 1

View File

@ -7,9 +7,9 @@ import (
) )
type VoteModelInterface interface { type VoteModelInterface interface {
Insert(voterIdentity string, electionId int, choiceText string, tokens int) (int, error) Insert(voterIdentity string, electionId string, choiceText string, tokens int) (int, error)
Exists(voterIdentity string, electionID int) (bool, error) Exists(voterIdentity string, electionID string) (bool, error)
GetByElection(electionID int) (*[]Vote, error) GetByElection(electionID string) (*[]Vote, error)
} }
type VoteModel struct { type VoteModel struct {
@ -18,13 +18,13 @@ type VoteModel struct {
type Vote struct { type Vote struct {
VoterIdentity string VoterIdentity string
ElectionID int ElectionID string
ChoiceText string ChoiceText string
Tokens int Tokens int
CreatedAt time.Time CreatedAt time.Time
} }
func (v *VoteModel) Insert(voterIdentity string, electionId int, choiceText string, tokens int) (int, error) { func (v *VoteModel) Insert(voterIdentity string, electionId string, choiceText string, tokens int) (int, error) {
tx, err := v.DB.Begin() tx, err := v.DB.Begin()
if err != nil { if err != nil {
return 0, err return 0, err
@ -47,7 +47,7 @@ func (v *VoteModel) Insert(voterIdentity string, electionId int, choiceText stri
return int(voteId), nil return int(voteId), nil
} }
func (v *VoteModel) Exists(voterIdentity string, electionID int) (bool, error) { func (v *VoteModel) Exists(voterIdentity string, electionID string) (bool, error) {
var exists bool var exists bool
query := ` query := `
SELECT EXISTS ( SELECT EXISTS (
@ -66,7 +66,7 @@ func (v *VoteModel) Exists(voterIdentity string, electionID int) (bool, error) {
return exists, nil return exists, nil
} }
func (v *VoteModel) GetByElection(electionID int) (*[]Vote, error) { func (v *VoteModel) GetByElection(electionID string) (*[]Vote, error) {
query := ` query := `
SELECT voter_identity, election_id, choice_text, tokens, created_at SELECT voter_identity, election_id, choice_text, tokens, created_at
FROM votes FROM votes

View File

@ -93,7 +93,7 @@
<template x-if="createdElectionId > 0"> <template x-if="createdElectionId > 0">
<div class="mt-6 bg-green-100 p-4 rounded-md"> <div class="mt-6 bg-green-100 p-4 rounded-md">
<h2 class="text-green-700 font-bold">Election Created Successfully</h2> <h2 class="text-green-700 font-bold">Election Created Successfully</h2>
<p class="mt-2">Election ID: <span class="font-mono" x-text="createdElectionId"></span></p> <p class="mt-2">Election ID: <span class="font-mono" x-text="createdElectionId"></span></p> <!-- TODO: link to created election -->
</div> </div>
</template> </template>