package main import ( "bytes" "code.dlmw.ch/dlmw/qv/internal" "code.dlmw.ch/dlmw/qv/internal/models" "database/sql" "encoding/json" "fmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "net/http" "testing" "time" ) func TestCreateElection(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) path := "/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: 10, 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, }, } 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_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 := "/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 TestCreateVotesUnknownVotersElection(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{ ID: 1, 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("Insert", mock.Anything, mock.Anything). Return(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 := "/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}, }, ElectionId: 1, 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}, }, ElectionId: 1, 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}, }, ElectionId: 1, 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 TestCreateVotesKnownVotersElection(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{ ID: 1, 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 := "/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}, }, ElectionId: 1, 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}, }, ElectionId: 1, VoterIdentity: &NON_EXISTING_VOTER_IDENTITY, }, 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}, }, ElectionId: 1, 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}, }, ElectionId: 1, 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 := "/votes" requestBody := api.CreateVotesRequest{ Choices: []struct { ChoiceText string `json:"choiceText"` Tokens int `json:"tokens"` }{ {ChoiceText: "Gandhi", Tokens: 60}, {ChoiceText: "Buddha", Tokens: 40}, }, ElectionId: 1, 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) }