diff --git a/TrueVote.Api.Tests/Helpers/TestHelper.cs b/TrueVote.Api.Tests/Helpers/TestHelper.cs index 6e74e3d..25761bf 100644 --- a/TrueVote.Api.Tests/Helpers/TestHelper.cs +++ b/TrueVote.Api.Tests/Helpers/TestHelper.cs @@ -40,6 +40,27 @@ public class TestHelper public const string MockedTokenValue = "mocked_token_value"; + public static ControllerContext AuthHelper(string userId) + { + // For endpoints that require Authorization [Authorize] + // Mock a user principal with desired claims + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, userId), + new Claim(ClaimTypes.Role, "User"), // Role + }; + var identity = new ClaimsIdentity(claims, "TestAuthentication"); + var principal = new ClaimsPrincipal(identity); + + // Create context for controllers to attach to + var authControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = principal } + }; + + return authControllerContext; + } + public TestHelper(ITestOutputHelper output) { // This will override the setup shims in Startup.cs @@ -92,22 +113,7 @@ public TestHelper(ITestOutputHelper output) _candidateApi = new Candidate(_logHelper.Object, _moqDataAccessor.mockCandidateContext.Object, _mockServiceBus.Object); _timestampApi = new Timestamp(_logHelper.Object, _moqDataAccessor.mockTimestampContext.Object); _queryService = new Query(_trueVoteDbContext); - - // For endpoints that require Authorization [Authorize] - // Mock a user principal with desired claims - var claims = new[] - { - new Claim(ClaimTypes.NameIdentifier, MoqData.MockUserData[0].UserId), - new Claim(ClaimTypes.Role, "User"), // Role - }; - var identity = new ClaimsIdentity(claims, "TestAuthentication"); - var principal = new ClaimsPrincipal(identity); - - // Create context for controllers to attach to - _authControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext { User = principal } - }; + _authControllerContext = AuthHelper(MoqData.MockUserData[0].UserId); } } } diff --git a/TrueVote.Api.Tests/MoqData.cs b/TrueVote.Api.Tests/MoqData.cs index b50d013..da5d2c6 100644 --- a/TrueVote.Api.Tests/MoqData.cs +++ b/TrueVote.Api.Tests/MoqData.cs @@ -1,9 +1,11 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; using MockQueryable.Moq; using Moq; using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using TrueVote.Api.Interfaces; using TrueVote.Api.Models; @@ -96,8 +98,13 @@ public static class MoqData public static List MockUsedAccessCodeData => new() { - new UsedAccessCodeModel { AccessCode = "accesscode1" }, - new UsedAccessCodeModel { AccessCode = "accesscode2" }, + new UsedAccessCodeModel { AccessCode = "accesscode1", DateCreated = createDate.Date }, + new UsedAccessCodeModel { AccessCode = "accesscode2", DateCreated = createDate.Date }, + }; + + public static List MockElectionUserBindingsData => new() + { + new ElectionUserBindingModel { UserId = MockUserData[2].UserId, ElectionId = MockElectionData[0].ElectionId, DateCreated = createDate.Date }, }; public static BallotList MockBallotList => new() @@ -119,6 +126,7 @@ public class MoqDataAccessor public readonly Mock mockFeedbacksContext; public readonly Mock mockElectionAccessCodeContext; public readonly Mock mockUsedAccessCodeContext; + public readonly Mock mockElectionUserBindingsContext; public Mock> MockUserSet { get; private set; } public Mock> MockRaceSet { get; private set; } @@ -130,6 +138,7 @@ public class MoqDataAccessor public Mock> MockFeedbackSet { get; private set; } public Mock> MockElectionAccessCodeSet { get; private set; } public Mock> MockUsedAccessCodeSet { get; private set; } + public Mock> MockElectionUserBindingsSet { get; private set; } // https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking?redirectedfrom=MSDN // https://github.com/romantitov/MockQueryable @@ -145,11 +154,11 @@ public MoqDataAccessor() MockFeedbackSet = MoqData.MockFeedbackData.AsQueryable().BuildMockDbSet(); MockElectionAccessCodeSet = MoqData.MockElectionAccessCodeData.AsQueryable().BuildMockDbSet(); MockUsedAccessCodeSet = MoqData.MockUsedAccessCodeData.AsQueryable().BuildMockDbSet(); + MockElectionUserBindingsSet = MoqData.MockElectionUserBindingsData.AsQueryable().BuildMockDbSet(); mockUserContext = new Mock(); mockUserContext.Setup(m => m.Feedbacks).Returns(MockFeedbackSet.Object); mockUserContext.Setup(m => m.Users).Returns(MockUserSet.Object); - mockUserContext.Setup(m => m.EnsureCreatedAsync()).Returns(Task.FromResult(true)); mockElectionContext = new Mock(); mockElectionContext.Setup(m => m.Elections).Returns(MockElectionSet.Object); @@ -157,15 +166,12 @@ public MoqDataAccessor() mockElectionContext.Setup(m => m.Users).Returns(MockUserSet.Object); mockElectionContext.Setup(m => m.ElectionAccessCodes).Returns(MockElectionAccessCodeSet.Object); mockElectionContext.Setup(m => m.UsedAccessCodes).Returns(MockUsedAccessCodeSet.Object); - mockElectionContext.Setup(m => m.EnsureCreatedAsync()).Returns(Task.FromResult(true)); mockTimestampContext = new Mock(); mockTimestampContext.Setup(m => m.Timestamps).Returns(MockTimestampSet.Object); - mockTimestampContext.Setup(m => m.EnsureCreatedAsync()).Returns(Task.FromResult(true)); mockBallotHashContext = new Mock(); mockBallotHashContext.Setup(m => m.BallotHashes).Returns(MockBallotHashSet.Object); - mockBallotHashContext.Setup(m => m.EnsureCreatedAsync()).Returns(Task.FromResult(true)); mockBallotContext = new Mock(); mockBallotContext.Setup(m => m.Elections).Returns(MockElectionSet.Object); @@ -176,28 +182,26 @@ public MoqDataAccessor() mockBallotContext.Setup(m => m.Timestamps).Returns(MockTimestampSet.Object); mockBallotContext.Setup(m => m.ElectionAccessCodes).Returns(MockElectionAccessCodeSet.Object); mockBallotContext.Setup(m => m.UsedAccessCodes).Returns(MockUsedAccessCodeSet.Object); - mockBallotContext.Setup(m => m.EnsureCreatedAsync()).Returns(Task.FromResult(true)); + mockBallotContext.Setup(m => m.ElectionUserBindings).Returns(MockElectionUserBindingsSet.Object); mockCandidateContext = new Mock(); mockCandidateContext.Setup(m => m.Candidates).Returns(MockCandidateSet.Object); - mockCandidateContext.Setup(m => m.EnsureCreatedAsync()).Returns(Task.FromResult(true)); mockRaceContext = new Mock(); mockRaceContext.Setup(m => m.Candidates).Returns(MockCandidateSet.Object); mockRaceContext.Setup(m => m.Races).Returns(MockRaceSet.Object); - mockRaceContext.Setup(m => m.EnsureCreatedAsync()).Returns(Task.FromResult(true)); mockFeedbacksContext = new Mock(); mockFeedbacksContext.Setup(m => m.Feedbacks).Returns(MockFeedbackSet.Object); - mockFeedbacksContext.Setup(m => m.EnsureCreatedAsync()).Returns(Task.FromResult(true)); mockElectionAccessCodeContext = new Mock(); mockElectionAccessCodeContext.Setup(m => m.ElectionAccessCodes).Returns(MockElectionAccessCodeSet.Object); - mockElectionAccessCodeContext.Setup(m => m.EnsureCreatedAsync()).Returns(Task.FromResult(true)); mockUsedAccessCodeContext = new Mock(); mockUsedAccessCodeContext.Setup(m => m.UsedAccessCodes).Returns(MockUsedAccessCodeSet.Object); - mockUsedAccessCodeContext.Setup(m => m.EnsureCreatedAsync()).Returns(Task.FromResult(true)); + + mockElectionUserBindingsContext = new Mock(); + mockElectionUserBindingsContext.Setup(m => m.ElectionUserBindings).Returns(MockElectionUserBindingsSet.Object); // Leaving commented code. This is for Mocking UTC time. Helpful for test consistency. // var mockUtcNowProvider = new Mock(); @@ -219,6 +223,7 @@ public class MoqTrueVoteDbContext : DbContext, ITrueVoteDbContext public virtual DbSet Feedbacks { get; set; } public virtual DbSet ElectionAccessCodes { get; set; } public virtual DbSet UsedAccessCodes { get; set; } + public virtual DbSet ElectionUserBindings { get; set; } protected MoqDataAccessor _moqDataAccessor; @@ -236,6 +241,7 @@ public MoqTrueVoteDbContext() Feedbacks = _moqDataAccessor.MockFeedbackSet.Object; ElectionAccessCodes = _moqDataAccessor.MockElectionAccessCodeSet.Object; UsedAccessCodes = _moqDataAccessor.MockUsedAccessCodeSet.Object; + ElectionUserBindings = _moqDataAccessor.MockElectionUserBindingsSet.Object; } public virtual async Task EnsureCreatedAsync() diff --git a/TrueVote.Api.Tests/ServiceTests/BallotTest.cs b/TrueVote.Api.Tests/ServiceTests/BallotTest.cs index f59a4f2..ac03629 100644 --- a/TrueVote.Api.Tests/ServiceTests/BallotTest.cs +++ b/TrueVote.Api.Tests/ServiceTests/BallotTest.cs @@ -28,6 +28,7 @@ public async Task SubmitsBallot() { var baseBallotObj = new SubmitBallotModel { AccessCode = MoqData.MockElectionAccessCodeData[0].AccessCode, Election = MoqData.MockBallotData[1].Election }; + _ballotApi.ControllerContext = _authControllerContext; var ret = await _ballotApi.SubmitBallot(baseBallotObj); Assert.NotNull(ret); Assert.Equal(StatusCodes.Status201Created, ((IStatusCodeActionResult) ret).StatusCode); @@ -113,6 +114,8 @@ public async Task HandlesSubmitBallotHashingError() mockValidator.Setup(m => m.HashBallotAsync(It.IsAny())).Throws(new Exception("Hash Ballot Exception")); var ballotApi = new Ballot(_logHelper.Object, _moqDataAccessor.mockBallotContext.Object, mockValidator.Object, _mockServiceBus.Object, _mockRecursiveValidator.Object); + + ballotApi.ControllerContext = _authControllerContext; var ret = await ballotApi.SubmitBallot(baseBallotObj); Assert.NotNull(ret); Assert.Equal(StatusCodes.Status409Conflict, ((IStatusCodeActionResult) ret).StatusCode); @@ -216,9 +219,12 @@ public async Task HandlesSubmitBallotDatabaseError() mockBallotContext.Setup(m => m.Ballots).Returns(MockBallotSet.Object); mockBallotContext.Setup(m => m.ElectionAccessCodes).Returns(_moqDataAccessor.MockElectionAccessCodeSet.Object); mockBallotContext.Setup(m => m.UsedAccessCodes).Returns(_moqDataAccessor.MockUsedAccessCodeSet.Object); + mockBallotContext.Setup(m => m.ElectionUserBindings).Returns(_moqDataAccessor.MockElectionUserBindingsSet.Object); mockBallotContext.Setup(m => m.SaveChangesAsync()).Throws(new Exception("DB Saving Changes Exception")); var ballotApi = new Ballot(_logHelper.Object, mockBallotContext.Object, _validatorApi, _mockServiceBus.Object, _mockRecursiveValidator.Object); + + ballotApi.ControllerContext = _authControllerContext; var ret = await ballotApi.SubmitBallot(baseBallotObj); Assert.NotNull(ret); Assert.Equal(StatusCodes.Status409Conflict, ((IStatusCodeActionResult) ret).StatusCode); @@ -236,6 +242,7 @@ public async Task HandlesSubmitBallotUnfoundAccessCode() { var baseBallotObj = new SubmitBallotModel { AccessCode = "blah", Election = MoqData.MockBallotData[1].Election }; + _ballotApi.ControllerContext = _authControllerContext; var ret = await _ballotApi.SubmitBallot(baseBallotObj); Assert.NotNull(ret); Assert.Equal(StatusCodes.Status404NotFound, ((IStatusCodeActionResult) ret).StatusCode); @@ -254,6 +261,7 @@ public async Task HandlesSubmitBallotAlreadyUsedAccessCode() { var baseBallotObj = new SubmitBallotModel { AccessCode = MoqData.MockUsedAccessCodeData[0].AccessCode, Election = MoqData.MockBallotData[1].Election }; + _ballotApi.ControllerContext = _authControllerContext; var ret = await _ballotApi.SubmitBallot(baseBallotObj); Assert.NotNull(ret); Assert.Equal(StatusCodes.Status409Conflict, ((IStatusCodeActionResult) ret).StatusCode); @@ -266,5 +274,23 @@ public async Task HandlesSubmitBallotAlreadyUsedAccessCode() _logHelper.Verify(LogLevel.Information, Times.Exactly(1)); _logHelper.Verify(LogLevel.Debug, Times.Exactly(2)); } + + [Fact] + public async Task HandlesSubmitBallotUserAlreadySubmitted() + { + var baseBallotObj = new SubmitBallotModel { AccessCode = MoqData.MockUsedAccessCodeData[0].AccessCode, Election = MoqData.MockBallotData[1].Election }; + + _ballotApi.ControllerContext = AuthHelper(MoqData.MockUserData[2].UserId); + var ret = await _ballotApi.SubmitBallot(baseBallotObj); + Assert.NotNull(ret); + Assert.Equal(StatusCodes.Status409Conflict, ((IStatusCodeActionResult) ret).StatusCode); + + var val = (SecureString) (ret as ConflictObjectResult).Value; + Assert.NotNull(val); + Assert.Contains("Ballot already submitted", val.Value); + + _logHelper.Verify(LogLevel.Information, Times.Exactly(1)); + _logHelper.Verify(LogLevel.Debug, Times.Exactly(2)); + } } } diff --git a/TrueVote.Api.Tests/ServiceTests/UserTest.cs b/TrueVote.Api.Tests/ServiceTests/UserTest.cs index 7a91480..114c223 100644 --- a/TrueVote.Api.Tests/ServiceTests/UserTest.cs +++ b/TrueVote.Api.Tests/ServiceTests/UserTest.cs @@ -246,7 +246,6 @@ public async Task SignInSuccessFoundUser() var mockUserDataCollection = mockUserData; var MockUserSet = DbMoqHelper.GetDbSet(mockUserDataQueryable); mockUserContext.Setup(m => m.Users).Returns(MockUserSet.Object); - mockUserContext.Setup(m => m.EnsureCreatedAsync()).Returns(Task.FromResult(true)); // Simulate a client (e.g. TypeScript) var content = new BaseUserModel diff --git a/TrueVote.Api.Tests/ServiceTests/ValidatorTest.cs b/TrueVote.Api.Tests/ServiceTests/ValidatorTest.cs index f1a1b38..4a7ab14 100644 --- a/TrueVote.Api.Tests/ServiceTests/ValidatorTest.cs +++ b/TrueVote.Api.Tests/ServiceTests/ValidatorTest.cs @@ -1,7 +1,9 @@ using Moq; using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; +using TrueVote.Api.Models; using TrueVote.Api.Services; using TrueVote.Api.Tests.Helpers; using Xunit; @@ -48,11 +50,14 @@ public async Task HashesBallotThrowsStoreTimestampException() var mockBallotContext = new Mock(); var mockBallotDataQueryable = MoqData.MockBallotData.AsQueryable(); var mockBallotHashDataQueryable = MoqData.MockBallotHashData.AsQueryable(); + var mockTimestampsDataQueryable = MoqData.MockTimestampData.AsQueryable(); var MockBallotSet = DbMoqHelper.GetDbSet(mockBallotDataQueryable); var MockBallotHashSet = DbMoqHelper.GetDbSet(mockBallotHashDataQueryable); + var MockTimestampsSet = DbMoqHelper.GetDbSet(mockTimestampsDataQueryable); mockBallotContext.Setup(m => m.Ballots).Returns(MockBallotSet.Object); mockBallotContext.Setup(m => m.BallotHashes).Returns(MockBallotHashSet.Object); - mockBallotContext.Setup(m => m.EnsureCreatedAsync()).Throws(new Exception("Storing data exception")); + mockBallotContext.Setup(m => m.Timestamps).Returns(MockTimestampsSet.Object); + mockBallotContext.Setup(m => m.SaveChangesAsync()).Throws(new Exception("Storing data exception")); var validatorApi = new BallotValidator(_logHelper.Object, mockBallotContext.Object, _mockOpenTimestampsClient.Object, _mockServiceBus.Object); @@ -103,10 +108,16 @@ public async Task HashesBallot() public async Task StoreBallotHashAsyncThrowsException() { var mockBallotHashContext = new Mock(); + var mockBallotDataQueryable = MoqData.MockBallotData.AsQueryable(); var mockBallotHashDataQueryable = MoqData.MockBallotHashData.AsQueryable(); + var mockTimestampsDataQueryable = MoqData.MockTimestampData.AsQueryable(); + var MockBallotSet = DbMoqHelper.GetDbSet(mockBallotDataQueryable); var MockBallotHashSet = DbMoqHelper.GetDbSet(mockBallotHashDataQueryable); + var MockTimestampsSet = DbMoqHelper.GetDbSet(mockTimestampsDataQueryable); + mockBallotHashContext.Setup(m => m.Ballots).Returns(MockBallotSet.Object); mockBallotHashContext.Setup(m => m.BallotHashes).Returns(MockBallotHashSet.Object); - mockBallotHashContext.Setup(m => m.EnsureCreatedAsync()).Throws(new Exception("Storing data exception")); + mockBallotHashContext.Setup(m => m.Timestamps).Returns(MockTimestampsSet.Object); + mockBallotHashContext.Setup(m => m.BallotHashes.AddAsync(It.IsAny(), It.IsAny())).Throws(new Exception("Storing data exception")); var validatorApi = new BallotValidator(_logHelper.Object, mockBallotHashContext.Object, _mockOpenTimestampsClient.Object, _mockServiceBus.Object); @@ -127,10 +138,16 @@ public async Task StoreBallotHashAsyncThrowsException() public async Task StoreTimestampAsyncThrowsException() { var mockTimestampContext = new Mock(); - var mockTimestampDataQueryable = MoqData.MockTimestampData.AsQueryable(); - var MockTimestampSet = DbMoqHelper.GetDbSet(mockTimestampDataQueryable); - mockTimestampContext.Setup(m => m.Timestamps).Returns(MockTimestampSet.Object); - mockTimestampContext.Setup(m => m.EnsureCreatedAsync()).Throws(new Exception("Storing data exception")); + var mockBallotDataQueryable = MoqData.MockBallotData.AsQueryable(); + var mockBallotHashDataQueryable = MoqData.MockBallotHashData.AsQueryable(); + var mockTimestampsDataQueryable = MoqData.MockTimestampData.AsQueryable(); + var MockBallotSet = DbMoqHelper.GetDbSet(mockBallotDataQueryable); + var MockBallotHashSet = DbMoqHelper.GetDbSet(mockBallotHashDataQueryable); + var MockTimestampsSet = DbMoqHelper.GetDbSet(mockTimestampsDataQueryable); + mockTimestampContext.Setup(m => m.Ballots).Returns(MockBallotSet.Object); + mockTimestampContext.Setup(m => m.BallotHashes).Returns(MockBallotHashSet.Object); + mockTimestampContext.Setup(m => m.Timestamps).Returns(MockTimestampsSet.Object); + mockTimestampContext.Setup(m => m.Timestamps.AddAsync(It.IsAny(), It.IsAny())).Throws(new Exception("Storing data exception")); var validatorApi = new BallotValidator(_logHelper.Object, mockTimestampContext.Object, _mockOpenTimestampsClient.Object, _mockServiceBus.Object); diff --git a/TrueVote.Api.Tests/TrueVote.Api.Tests.csproj b/TrueVote.Api.Tests/TrueVote.Api.Tests.csproj index 7d54e6b..4a68b5a 100644 --- a/TrueVote.Api.Tests/TrueVote.Api.Tests.csproj +++ b/TrueVote.Api.Tests/TrueVote.Api.Tests.csproj @@ -19,8 +19,8 @@ - - + + diff --git a/TrueVote.Api/Interfaces/ITrueVoteDbContext.cs b/TrueVote.Api/Interfaces/ITrueVoteDbContext.cs index 193da9f..99b65e6 100644 --- a/TrueVote.Api/Interfaces/ITrueVoteDbContext.cs +++ b/TrueVote.Api/Interfaces/ITrueVoteDbContext.cs @@ -15,6 +15,7 @@ public interface ITrueVoteDbContext DbSet Feedbacks { get; set; } DbSet ElectionAccessCodes { get; set; } DbSet UsedAccessCodes { get; set; } + DbSet ElectionUserBindings { get; set; } Task EnsureCreatedAsync(); Task SaveChangesAsync(); diff --git a/TrueVote.Api/Models/ElectionModel.cs b/TrueVote.Api/Models/ElectionModel.cs index ef22dbf..e57f1b2 100644 --- a/TrueVote.Api/Models/ElectionModel.cs +++ b/TrueVote.Api/Models/ElectionModel.cs @@ -202,9 +202,8 @@ public class AccessCodeModel public required string AccessCode { get; set; } } - // This model is bare bones, with no timestamp. Because if we were to store a timestamp, heuristics could be used to see the timestamp of this Date and match it to the Ballot Date, - // and then determine the access code used to submit the ballot. With that info, the user that submitted the ballot could be determined. - // So we just store the raw access code in a table to determine if it was used or not. + // For DateCreated, only going to store YYYYMMDD, not the time. Because if we stored a very precise date, + // it would be possible to bind to a BallotId which could reveal the User by using heuristics. public class UsedAccessCodeModel { [Required] @@ -215,6 +214,43 @@ public class UsedAccessCodeModel [JsonProperty(nameof(AccessCode), Required = Required.Always)] [Key] public required string AccessCode { get; set; } + + [Required] + [Description("DateCreated")] + [DataType(DataType.Date)] + [JsonPropertyName("DateCreated")] + [JsonProperty(nameof(DateCreated), Required = Required.Default)] + public required DateTime DateCreated { get; set; } + } + + // For DateCreated, only going to store YYYYMMDD, not the time. Because if we stored a very precise date, + // it would be possible to bind to a BallotId which could reveal the User by using heuristics. + public class ElectionUserBindingModel + { + [Required] + [Description("Election Id")] + [MaxLength(2048)] + [DataType(DataType.Text)] + [JsonPropertyName("ElectionId")] + [JsonProperty(nameof(ElectionId), Required = Required.Always)] + [Key] + public required string ElectionId { get; set; } + + [Required] + [Description("User Id")] + [MaxLength(2048)] + [DataType(DataType.Text)] + [JsonPropertyName("UserId")] + [JsonProperty(nameof(UserId), Required = Required.Always)] + [Key] + public required string UserId { get; set; } + + [Required] + [Description("DateCreated")] + [DataType(DataType.Date)] + [JsonPropertyName("DateCreated")] + [JsonProperty(nameof(DateCreated), Required = Required.Default)] + public required DateTime DateCreated { get; set; } } public class AccessCodesResponse diff --git a/TrueVote.Api/Services/Ballot.cs b/TrueVote.Api/Services/Ballot.cs index 79ca304..77330bb 100644 --- a/TrueVote.Api/Services/Ballot.cs +++ b/TrueVote.Api/Services/Ballot.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Security.Claims; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -67,14 +68,25 @@ public async Task SubmitBallot([FromBody] SubmitBallotModel bindS return ValidationProblem(new ValidationProblemDetails(errorDictionary)); } - // Check if access code has been used or is invalid - var usedAccessCode = new UsedAccessCodeModel { AccessCode = bindSubmitBallotModel.AccessCode }; + // Check if user already submitted ballot for this election + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + var alreadySubmitted = await _trueVoteDbContext.ElectionUserBindings.Where(u => u.UserId == userId && u.ElectionId == bindSubmitBallotModel.Election.ElectionId).FirstOrDefaultAsync(); + if (alreadySubmitted != null) + { + _log.LogDebug("HTTP trigger - SubmitBallot:End"); + return Conflict(new SecureString { Value = $"Ballot already submitted for User" }); + } + + var now = UtcNowProviderFactory.GetProvider().UtcNow; + + // Check if access code has been used or is invalid. Note timestamp only stores the .Date, not the time. + var usedAccessCode = new UsedAccessCodeModel { AccessCode = bindSubmitBallotModel.AccessCode, DateCreated = now.Date }; // Determine if the EAC exists var accessCode = await _trueVoteDbContext.ElectionAccessCodes.Where(u => u.AccessCode == usedAccessCode.AccessCode).FirstOrDefaultAsync(); if (accessCode == null) { - _log.LogDebug("Non-Public Function - UseAccessCode:End"); + _log.LogDebug("HTTP trigger - SubmitBallot:End"); return NotFound(new SecureString { Value = $"AccessCode: '{usedAccessCode.AccessCode}' not found" }); } @@ -82,10 +94,11 @@ public async Task SubmitBallot([FromBody] SubmitBallotModel bindS var alreadyUsed = await _trueVoteDbContext.UsedAccessCodes.Where(u => u.AccessCode == usedAccessCode.AccessCode).FirstOrDefaultAsync(); if (alreadyUsed != null) { - _log.LogDebug("Non-Public Function - UseAccessCode:End"); + _log.LogDebug("HTTP trigger - SubmitBallot:End"); return Conflict(new SecureString { Value = $"AccessCode: '{usedAccessCode.AccessCode}' already used" }); } - var ballot = new BallotModel { Election = bindSubmitBallotModel.Election, BallotId = Guid.NewGuid().ToString(), DateCreated = UtcNowProviderFactory.GetProvider().UtcNow }; + + var ballot = new BallotModel { Election = bindSubmitBallotModel.Election, BallotId = Guid.NewGuid().ToString(), DateCreated = now }; // TODO Localize .Message var submitBallotResponse = new SubmitBallotModelResponse @@ -95,11 +108,19 @@ public async Task SubmitBallot([FromBody] SubmitBallotModel bindS Message = $"Election ID: {bindSubmitBallotModel.Election.ElectionId}, Ballot ID: {ballot.BallotId}" }; + var electionUserBindingModel = new ElectionUserBindingModel + { + ElectionId = bindSubmitBallotModel.Election.ElectionId, + UserId = userId, + DateCreated = now.Date + }; + try { await _trueVoteDbContext.EnsureCreatedAsync(); await _trueVoteDbContext.Ballots.AddAsync(ballot); await _trueVoteDbContext.UsedAccessCodes.AddAsync(usedAccessCode); + await _trueVoteDbContext.ElectionUserBindings.AddAsync(electionUserBindingModel); await _trueVoteDbContext.SaveChangesAsync(); } catch (Exception e) diff --git a/TrueVote.Api/Services/BallotValidator.cs b/TrueVote.Api/Services/BallotValidator.cs index 9c00041..83ce028 100644 --- a/TrueVote.Api/Services/BallotValidator.cs +++ b/TrueVote.Api/Services/BallotValidator.cs @@ -132,7 +132,6 @@ public async virtual Task StoreTimestampAsync(TimestampModel timestamp) { try { - await _trueVoteDbContext.EnsureCreatedAsync(); await _trueVoteDbContext.Timestamps.AddAsync(timestamp); } catch (Exception ex) @@ -146,7 +145,6 @@ public async virtual Task StoreBallotHashAsync(BallotHashModel ballotHashModel) { try { - await _trueVoteDbContext.EnsureCreatedAsync(); await _trueVoteDbContext.BallotHashes.AddAsync(ballotHashModel); } catch (Exception ex) diff --git a/TrueVote.Api/Services/Candidate.cs b/TrueVote.Api/Services/Candidate.cs index 70b6e27..5c503c1 100644 --- a/TrueVote.Api/Services/Candidate.cs +++ b/TrueVote.Api/Services/Candidate.cs @@ -41,8 +41,6 @@ public async Task CreateCandidate([FromBody] BaseCandidateModel b var candidate = baseCandidate.DTOToCandidate(); - await _trueVoteDbContext.EnsureCreatedAsync(); - await _trueVoteDbContext.Candidates.AddAsync(candidate); await _trueVoteDbContext.SaveChangesAsync(); diff --git a/TrueVote.Api/Services/Election.cs b/TrueVote.Api/Services/Election.cs index 8880a9e..974f34a 100644 --- a/TrueVote.Api/Services/Election.cs +++ b/TrueVote.Api/Services/Election.cs @@ -43,8 +43,6 @@ public async Task CreateElection([FromBody] BaseElectionModel bas var races = baseElection.BaseRaces.DTOToRaces(); var election = baseElection.DTOToElection(races); - await _trueVoteDbContext.EnsureCreatedAsync(); - await _trueVoteDbContext.Elections.AddAsync(election); await _trueVoteDbContext.SaveChangesAsync(); @@ -122,8 +120,6 @@ public async Task AddRaces([FromBody] AddRacesModel bindRaceElect election.DateCreated = UtcNowProviderFactory.GetProvider().UtcNow; election.ElectionId = Guid.NewGuid().ToString(); - await _trueVoteDbContext.EnsureCreatedAsync(); - await _trueVoteDbContext.Elections.AddAsync(election); await _trueVoteDbContext.SaveChangesAsync(); @@ -177,8 +173,6 @@ public async Task CreateAccessCodes([FromBody] AccessCodesRequest AccessCodes = [] }; - await _trueVoteDbContext.EnsureCreatedAsync(); - for (var i = 0; i < accessCodesRequest.NumberOfAccessCodes; i++) { var uniqueKey = UniqueKeyGenerator.GenerateUniqueKey(); diff --git a/TrueVote.Api/Services/Race.cs b/TrueVote.Api/Services/Race.cs index 481736f..8feb5a5 100644 --- a/TrueVote.Api/Services/Race.cs +++ b/TrueVote.Api/Services/Race.cs @@ -42,8 +42,6 @@ public async Task CreateRace([FromBody] BaseRaceModel baseRace) var race = baseRace.DTOToRace(); - await _trueVoteDbContext.EnsureCreatedAsync(); - await _trueVoteDbContext.Races.AddAsync(race); await _trueVoteDbContext.SaveChangesAsync(); @@ -97,8 +95,6 @@ public async Task AddCandidates([FromBody] AddCandidatesModel add race.DateCreated = UtcNowProviderFactory.GetProvider().UtcNow; race.RaceId = Guid.NewGuid().ToString(); - await _trueVoteDbContext.EnsureCreatedAsync(); - await _trueVoteDbContext.Races.AddAsync(race); await _trueVoteDbContext.SaveChangesAsync(); diff --git a/TrueVote.Api/Services/User.cs b/TrueVote.Api/Services/User.cs index fd97cff..5e072ff 100644 --- a/TrueVote.Api/Services/User.cs +++ b/TrueVote.Api/Services/User.cs @@ -186,8 +186,6 @@ private async Task AddNewUser(BaseUserModel baseUser, string userId) var now = UtcNowProviderFactory.GetProvider().UtcNow; var user = new UserModel { FullName = baseUser.FullName, Email = baseUser.Email, UserId = userId, NostrPubKey = baseUser.NostrPubKey, DateCreated = now, DateUpdated = now, UserPreferences = new UserPreferencesModel() }; - await _trueVoteDbContext.EnsureCreatedAsync(); - await _trueVoteDbContext.Users.AddAsync(user); await _trueVoteDbContext.SaveChangesAsync(); @@ -282,8 +280,6 @@ public async Task SaveFeedback([FromBody] FeedbackModel feedback) feedback.FeedbackId = Guid.NewGuid().ToString(); feedback.DateCreated = UtcNowProviderFactory.GetProvider().UtcNow; - await _trueVoteDbContext.EnsureCreatedAsync(); - await _trueVoteDbContext.Feedbacks.AddAsync(feedback); await _trueVoteDbContext.SaveChangesAsync(); diff --git a/TrueVote.Api/Startup.cs b/TrueVote.Api/Startup.cs index 9ca105b..14d7c27 100644 --- a/TrueVote.Api/Startup.cs +++ b/TrueVote.Api/Startup.cs @@ -158,7 +158,7 @@ public void ConfigureServices(IServiceCollection services) services.AddProblemDetails(); } - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, TrueVoteDbContext dbContext) { if (env.IsDevelopment()) { @@ -220,6 +220,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) e.MapSwagger(); }); + dbContext.EnsureCreatedAsync().GetAwaiter().GetResult(); + Console.WriteLine("HostingEnvironmentName: '{0}'", env.EnvironmentName); } @@ -235,6 +237,7 @@ public class TrueVoteDbContext : DbContext, ITrueVoteDbContext public virtual DbSet Feedbacks { get; set; } public virtual DbSet ElectionAccessCodes { get; set; } public virtual DbSet UsedAccessCodes { get; set; } + public virtual DbSet ElectionUserBindings { get; set; } private readonly IConfiguration? _configuration; private readonly string? _connectionString; @@ -329,6 +332,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().ToContainer("UsedAccessCodes"); modelBuilder.Entity().HasNoDiscriminator(); modelBuilder.Entity().HasKey(ac => new { ac.AccessCode }); + + modelBuilder.HasDefaultContainer("ElectionUserBindings"); + modelBuilder.Entity().ToContainer("ElectionUserBindings"); + modelBuilder.Entity().HasNoDiscriminator(); + modelBuilder.Entity().HasKey(eub => new { eub.UserId, eub.ElectionId }); } } diff --git a/TrueVote.Api/TrueVote.Api.csproj b/TrueVote.Api/TrueVote.Api.csproj index 3b25158..b75a2d9 100644 --- a/TrueVote.Api/TrueVote.Api.csproj +++ b/TrueVote.Api/TrueVote.Api.csproj @@ -26,8 +26,8 @@ - - + +