package main import ( "bytes" "code.dlmw.ch/dlmw/qv/internal" "code.dlmw.ch/dlmw/qv/internal/models" "database/sql" "encoding/json" "fmt" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "net/http" "testing" "time" ) const baseUri = "/api/" func TestCreateElection(t *testing.T) { app := newTestApplication(t) server := newTestServer(t, app.routes()) defer server.Close() id, _ := uuid.NewV7() mockElections := app.elections.(*mockElectionModel) mockElections. On("Insert", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(id.String(), nil) path := baseUri + "election" tests := []struct { name string urlPath string body any expectedCode int }{ { name: "Valid request (small name, other language)", urlPath: path, body: api.CreateElectionRequest{ Choices: []string{"宮本武蔵", "伊東一刀斎"}, ExpiresAt: time.Now().Add(24 * time.Hour), AreVotersKnown: false, MaxVoters: 0, Name: "強", Tokens: 100, }, expectedCode: http.StatusOK, }, { name: "Valid request (voters unknown with unlimited voters)", urlPath: path, body: api.CreateElectionRequest{ Choices: []string{"Gandhi", "Buddha"}, ExpiresAt: time.Now().Add(24 * time.Hour), AreVotersKnown: false, MaxVoters: 0, Name: "Guy of the year", Tokens: 100, }, expectedCode: http.StatusOK, }, { name: "Valid request (with 3 choices)", urlPath: path, body: api.CreateElectionRequest{ Choices: []string{"Gandhi", "Buddha", "You"}, ExpiresAt: time.Now().Add(24 * time.Hour), AreVotersKnown: false, MaxVoters: 0, Name: "Guy of the year", Tokens: 100, }, expectedCode: http.StatusOK, }, { name: "Valid request (voters unknown with max voters)", urlPath: path, body: api.CreateElectionRequest{ Choices: []string{"Gandhi", "Buddha"}, ExpiresAt: time.Now().Add(24 * time.Hour), AreVotersKnown: false, MaxVoters: 1000, Name: "Guy of the year", Tokens: 100, }, expectedCode: http.StatusOK, }, { name: "Valid request (voters known with max voters)", urlPath: path, body: api.CreateElectionRequest{ Choices: []string{"Gandhi", "Buddha"}, ExpiresAt: time.Now().Add(24 * time.Hour), AreVotersKnown: false, MaxVoters: 10, Name: "Guy of the year", Tokens: 100, }, expectedCode: http.StatusOK, }, { name: "Invalid request (not enough choices)", urlPath: path, body: api.CreateElectionRequest{ Choices: []string{"Gandhi"}, ExpiresAt: time.Unix(0, 0), AreVotersKnown: false, MaxVoters: 0, Name: "Guy of the year", Tokens: 100, }, expectedCode: http.StatusUnprocessableEntity, }, { name: "Invalid request (expiresAt is not in the future)", urlPath: path, body: api.CreateElectionRequest{ Choices: []string{"Gandhi", "Buddha"}, ExpiresAt: time.Unix(0, 0), AreVotersKnown: false, MaxVoters: 0, Name: "Guy of the year", Tokens: 100, }, expectedCode: http.StatusUnprocessableEntity, }, { name: "Invalid request (max voters must be greater than 0 for known elections)", urlPath: path, body: api.CreateElectionRequest{ Choices: []string{"Gandhi", "Buddha"}, ExpiresAt: time.Unix(0, 0), AreVotersKnown: true, MaxVoters: 0, Name: "Guy of the year", Tokens: 100, }, expectedCode: http.StatusUnprocessableEntity, }, { name: "Invalid request (blank name)", urlPath: path, body: api.CreateElectionRequest{ Choices: []string{"Gandhi", "Buddha"}, ExpiresAt: time.Unix(0, 0), AreVotersKnown: true, MaxVoters: 0, Name: "", Tokens: 100, }, expectedCode: http.StatusUnprocessableEntity, }, { name: "Invalid request (choices are not unique)", urlPath: path, body: api.CreateElectionRequest{ Choices: []string{"Gandhi", "Gandhi"}, ExpiresAt: time.Now().Add(24 * time.Hour), AreVotersKnown: false, MaxVoters: 0, Name: "Guy of the year", Tokens: 100, }, expectedCode: http.StatusUnprocessableEntity, }, { name: "Invalid request (choices contain blank entries)", urlPath: path, body: api.CreateElectionRequest{ Choices: []string{"Gandhi", "Buddha", ""}, ExpiresAt: time.Now().Add(24 * time.Hour), AreVotersKnown: false, MaxVoters: 0, Name: "Guy of the year", Tokens: 100, }, expectedCode: http.StatusUnprocessableEntity, }, { name: "Invalid request for known voters election (max voters greater than 100)", urlPath: path, body: api.CreateElectionRequest{ Choices: []string{"Gandhi", "Buddha", ""}, ExpiresAt: time.Now().Add(24 * time.Hour), AreVotersKnown: true, MaxVoters: 101, Name: "Guy of the year", Tokens: 100, }, 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 TestCreateElection_KnownVoters(t *testing.T) { app := newTestApplication(t) server := newTestServer(t, app.routes()) defer server.Close() electionId, _ := uuid.NewV7() mockElections := app.elections.(*mockElectionModel) mockElections. On("Insert", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(electionId.String(), 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()) defer server.Close() mockElections := app.elections.(*mockElectionModel) mockElections. On("Insert", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(0, fmt.Errorf("")) path := baseUri + "election" requestBody := api.CreateElectionRequest{ Choices: []string{"宮本武蔵", "伊東一刀斎"}, ExpiresAt: time.Now().Add(24 * time.Hour), AreVotersKnown: false, MaxVoters: 0, Name: "強", Tokens: 100, } requestBodyJson, err := json.Marshal(requestBody) if err != nil { t.Fatal(err) } code, _, _ := server.post(t, path, bytes.NewReader(requestBodyJson)) assert.Equal(t, 500, code) } func TestCreateVotes_UnknownVotersElection(t *testing.T) { app := newTestApplication(t) server := newTestServer(t, app.routes()) defer server.Close() id, _ := uuid.NewV7() mockElections := app.elections.(*mockElectionModel) mockElections. On("GetById", mock.Anything). Return(&models.Election{ ID: id.String(), 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("InsertMultiple", mock.Anything, mock.Anything). Return([]int{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 := baseUri + "election/1/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}, }, 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}, }, 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}, }, 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 TestCreateVotes_KnownVotersElection(t *testing.T) { app := newTestApplication(t) server := newTestServer(t, app.routes()) defer server.Close() id, _ := uuid.NewV7() mockElections := app.elections.(*mockElectionModel) mockElections. On("GetById", mock.Anything). Return(&models.Election{ ID: id.String(), 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 := baseUri + "election/1/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}, }, 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}, }, VoterIdentity: &NON_EXISTING_VOTER_IDENTITY, }, expectedCode: http.StatusUnprocessableEntity, }, { name: "Invalid request for unknown voters election (no voter identity provided)", urlPath: path, body: api.CreateVotesRequest{ Choices: []struct { ChoiceText string `json:"choiceText"` Tokens int `json:"tokens"` }{ {ChoiceText: "Gandhi", Tokens: 60}, {ChoiceText: "Buddha", Tokens: 40}, }, VoterIdentity: nil, }, 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}, }, 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}, }, 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 := baseUri + "election/1/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.StatusNotFound, code) } func TestCreateVotes_NonUuidElectionID(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 := baseUri + "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.StatusNotFound, code) } func TestCreateVotes_AlreadyVoted(t *testing.T) { app := newTestApplication(t) server := newTestServer(t, app.routes()) defer server.Close() unknownVotersElectionId, _ := uuid.NewV7() unknownVotersElection := models.Election{ ID: unknownVotersElectionId.String(), Name: "Guy of the year", Tokens: 100, AreVotersKnown: false, MaxVoters: 10, CreatedAt: time.Now(), ExpiresAt: time.Now().Add(24 * time.Hour), Choices: []string{"Gandhi", "Buddha"}, } knownVotersElection := unknownVotersElection knownVotersElectionId, _ := uuid.NewV7() knownVotersElection.ID = knownVotersElectionId.String() knownVotersElection.AreVotersKnown = true mockElections := app.elections.(*mockElectionModel) mockElections. On("GetById", knownVotersElectionId.String()). Return(&knownVotersElection, nil) mockElections. On("GetById", unknownVotersElectionId.String()). Return(&unknownVotersElection, nil) mockVotes := app.votes.(*mockVoteModel) mockVotes. On("Exists", mock.Anything, mock.Anything). Return(true, nil) mockVoters := app.voters.(*mockVoterModel) mockVoters. On("Exists", mock.Anything, mock.Anything). Return(true, nil) layout := baseUri + "election/%v/votes" knownVotersElectionPath := fmt.Sprintf(layout, unknownVotersElectionId) unknownVotersElectionPath := fmt.Sprintf(layout, knownVotersElectionId) voterIdentity := "anything" tests := []struct { name string urlPath string body any expectedCode int }{ { name: "Invalid request for known voters election (already voted)", urlPath: knownVotersElectionPath, body: api.CreateVotesRequest{ Choices: []struct { ChoiceText string `json:"choiceText"` Tokens int `json:"tokens"` }{ {ChoiceText: "Gandhi", Tokens: 60}, {ChoiceText: "Buddha", Tokens: 40}, }, VoterIdentity: &voterIdentity, }, expectedCode: http.StatusUnprocessableEntity, }, { name: "Invalid request for unknown voters election (already voted)", urlPath: unknownVotersElectionPath, body: api.CreateVotesRequest{ Choices: []struct { ChoiceText string `json:"choiceText"` Tokens int `json:"tokens"` }{ {ChoiceText: "Gandhi", Tokens: 60}, {ChoiceText: "Buddha", Tokens: 40}, }, VoterIdentity: &voterIdentity, }, 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_UnknownVotersElectionMaxVotersReached(t *testing.T) { app := newTestApplication(t) server := newTestServer(t, app.routes()) defer server.Close() id, _ := uuid.NewV7() mockElections := app.elections.(*mockElectionModel) mockElections. On("GetById", mock.Anything). Return(&models.Election{ ID: id.String(), Name: "Guy of the year", Tokens: 100, AreVotersKnown: false, MaxVoters: 10, 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("InsertMultiple", mock.Anything, mock.Anything). Return([]int{1}, nil) mockVoters. On("CountByElection", mock.Anything). Return(10, nil) path := baseUri + "election/1/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.StatusUnprocessableEntity, code) } func TestCreateVotes_ExpiredElection(t *testing.T) { app := newTestApplication(t) server := newTestServer(t, app.routes()) defer server.Close() id, _ := uuid.NewV7() mockElections := app.elections.(*mockElectionModel) mockElections. On("GetById", mock.Anything). Return(&models.Election{ ID: id.String(), Name: "Guy of the year", Tokens: 100, AreVotersKnown: false, MaxVoters: 10, CreatedAt: time.UnixMilli(1), ExpiresAt: time.UnixMilli(100000), 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(10, nil) path := baseUri + "election/1/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.StatusUnprocessableEntity, code) } func TestGetElectionResults(t *testing.T) { app := newTestApplication(t) server := newTestServer(t, app.routes()) defer server.Close() electionID, _ := uuid.NewV7() votes := []models.Vote{ { VoterIdentity: "Voter1", ElectionID: electionID.String(), ChoiceText: "Choice1", Tokens: 2, CreatedAt: time.Now(), }, { VoterIdentity: "Voter2", ElectionID: electionID.String(), ChoiceText: "Choice2", Tokens: 4, CreatedAt: time.Now(), }, { VoterIdentity: "Voter3", ElectionID: electionID.String(), ChoiceText: "Choice3", Tokens: 6, CreatedAt: time.Now(), }, { VoterIdentity: "Voter4", ElectionID: electionID.String(), ChoiceText: "Choice1", Tokens: 8, CreatedAt: time.Now(), }, { VoterIdentity: "Voter5", ElectionID: electionID.String(), ChoiceText: "Choice2", Tokens: 10, CreatedAt: time.Now(), }, } mockVotes := app.votes.(*mockVoteModel) mockVotes. On("GetByElection", electionID.String()). Return(&votes, nil) path := baseUri + fmt.Sprintf("election/%v/results", electionID) code, _, body := server.get(t, path) assert.Equal(t, http.StatusOK, code) var response api.ElectionResultsResponse err := json.Unmarshal([]byte(body), &response) if err != nil { t.Fatal(err) } for _, result := range *response.Results { switch result.Choice { case "Choice1": assert.Equal(t, 3, result.Votes) case "Choice2": assert.Equal(t, 5, result.Votes) case "Choice3": assert.Equal(t, 2, result.Votes) default: t.Fatalf("Unexpected choice: %s", result.Choice) } } } func TestGetElectionResults_NotFound(t *testing.T) { app := newTestApplication(t) server := newTestServer(t, app.routes()) defer server.Close() mockVotes := app.votes.(*mockVoteModel) mockVotes. On("GetByElection", mock.Anything). Return(&[]models.Vote{}, sql.ErrNoRows) path := baseUri + "election/1/results" code, _, body := server.get(t, path) assert.Equal(t, http.StatusNotFound, code) var response api.ElectionResultsResponse err := json.Unmarshal([]byte(body), &response) if err != nil { t.Fatal(err) } } func TestGetElection_Found(t *testing.T) { app := newTestApplication(t) server := newTestServer(t, app.routes()) defer server.Close() id, _ := uuid.NewV7() mockElections := app.elections.(*mockElectionModel) mockElections. On("GetById", mock.Anything). Return(&models.Election{ ID: id.String(), Name: "Guy of the year", Tokens: 100, AreVotersKnown: false, MaxVoters: 10, CreatedAt: time.UnixMilli(1), ExpiresAt: time.UnixMilli(100000), Choices: []string{"Gandhi", "Buddha"}, }, nil) path := baseUri + "election/1" code, _, _ := server.get(t, path) assert.Equal(t, http.StatusOK, code) } func TestGetElection_NotFound(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 := baseUri + "election/1" code, _, _ := server.get(t, path) assert.Equal(t, http.StatusNotFound, code) }