diff --git a/src/Client/Pages/Admin/AdminPage.razor b/src/Client/Pages/Admin/AdminPage.razor index 3f23c206..ba70de64 100644 --- a/src/Client/Pages/Admin/AdminPage.razor +++ b/src/Client/Pages/Admin/AdminPage.razor @@ -4,36 +4,43 @@

Adminsida

- - - Hantera kontrakt - Hantera användare - - - - - - - - - - - - + +
+ + + Hantera kontrakt + Hantera användare + + + + + + + + + + + + +
@code { private string _selectedTab = "contracts"; + // Null-forgiving operator used because these fields are set by the Blazor runtime using @ref private ContractTable _contractTable = null!; private UserTable _userTable = null!; + private ContractForm _contractForm = null!; private void AddContractToTable(Contract contract) { - _contractTable.Add(contract); + _contractTable.AddOrUpdate(contract); } private void AddUserToTable(User user) @@ -46,4 +53,10 @@ _selectedTab = tabName; return Task.CompletedTask; } + + private async Task OnContractOpeningForEdit(Contract contract) + { + await _contractForm.EditContractAsync(contract); + } + } diff --git a/src/Client/Pages/Admin/AdminPage.razor.css b/src/Client/Pages/Admin/AdminPage.razor.css new file mode 100644 index 00000000..86ba7676 --- /dev/null +++ b/src/Client/Pages/Admin/AdminPage.razor.css @@ -0,0 +1,4 @@ +::deep .nav-link { + background: #e7e7e7; + color: var(--prodigo-dark-blue) +} diff --git a/src/Client/Pages/Admin/ContractForm.razor b/src/Client/Pages/Admin/ContractForm.razor index d67ec07f..7d4d9adc 100644 --- a/src/Client/Pages/Admin/ContractForm.razor +++ b/src/Client/Pages/Admin/ContractForm.razor @@ -6,8 +6,9 @@ @using Blazorise.Markdown @inject ILogger _logger @inject HttpClient _http +@inject IJSRuntime _js -

Skapa nytt kontrakt

+

Skapa nytt kontrakt

@@ -101,6 +102,38 @@ private const string MinFormHeight = "5rem"; + /// + /// Populates the form with the values of the given . + /// + /// The data to edit. + public async Task EditContractAsync(Contract contract) + { + await _js.InvokeVoidAsync("scrollToElement", "#contract-form-title"); + _shouldRender = true; + _contract = contract; + await DownloadContractFiles(); + } + + private async Task DownloadContractFiles() + { + await DownloadContractFile(_contract.InspirationalImagePath, content => _inspirationalContent = content); + await DownloadContractFile(_contract.SupplierLogoImagePath, content => _supplierLogoContent = content); + await DownloadContractFile(_contract.AdditionalDocument, content => _additionalDocumentContent = content); + } + + private async Task DownloadContractFile(string path, Action setContent) + { + + var fileUri = new Uri(Path.Join(_http.BaseAddress?.ToString(), path[1..]), UriKind.Absolute); + + HttpResponseMessage response = await _http.GetAsync(fileUri); + string mediaType = response.Content.Headers.ContentType?.MediaType ?? "images/jpeg"; + Stream stream = await response.Content.ReadAsStreamAsync(); + + MultipartFormDataContent content = CreateFormDataContent(stream, fileUri.Segments.Last(), mediaType); + setContent(content); + } + private static Contract CreateEmptyContract() { return new Contract @@ -126,34 +159,37 @@ { _shouldRender = false; - content = new MultipartFormDataContent(); - StreamContent fileContent; - const long bitsInAKilobyte = 1024; const long kilobytesInAMegabyte = 1024; const long maxFileSize = bitsInAKilobyte * kilobytesInAMegabyte * 100; + Stream fileStream = arg.File.OpenReadStream(maxFileSize); + + content = CreateFormDataContent(fileStream, arg.File.Name, arg.File.ContentType); + + _shouldRender = true; + } + + private MultipartFormDataContent CreateFormDataContent(Stream fileStream, string fileName, string contentType) + { + StreamContent fileContent; + try { - fileContent = new StreamContent(arg.File.OpenReadStream(maxFileSize)); + fileContent = new StreamContent(fileStream); } catch (IOException ex) { - _logger.LogInformation( - "{FileName} not uploaded: {Message}", - arg.File.Name, ex.Message); - return; + _logger.LogInformation("{FileName} not uploaded: {Message}", fileName, ex.Message); + throw; } - fileContent.Headers.ContentType = - new MediaTypeHeaderValue(arg.File.ContentType); + fileContent.Headers.ContentType = new MediaTypeHeaderValue(contentType); - content.Add( - fileContent, - "\"file\"", - arg.File.Name); + var content = new MultipartFormDataContent(); + content.Add(fileContent, "\"file\"", fileName); - _shouldRender = true; + return content; } private async Task OnSubmit() @@ -192,12 +228,11 @@ { string json = JsonConvert.SerializeObject(_contract); var content = new StringContent(json, Encoding.UTF8, "application/json"); - HttpResponseMessage response = await _http.PostAsync("/api/v1/contracts", content); + HttpResponseMessage response = await _http.PutAsync($"/api/v1/contracts/{_contract.Id}", content); if (response.IsSuccessStatusCode) { await OnContractUploaded.InvokeAsync(_contract); _contract = CreateEmptyContract(); } } - } diff --git a/src/Client/Pages/Admin/ContractTable.razor b/src/Client/Pages/Admin/ContractTable.razor index f9270292..b349b5a7 100644 --- a/src/Client/Pages/Admin/ContractTable.razor +++ b/src/Client/Pages/Admin/ContractTable.razor @@ -12,6 +12,7 @@ Kontraktnamn Företag + Redigera Ta bort @@ -19,7 +20,10 @@ @foreach (Contract contract in contracts) { - + } @@ -28,18 +32,35 @@ @code { - private FetchData> _dataFetcher = null!; + /// + /// Called when a is being opened to be edited. + /// + [Parameter] + public EventCallback ContractOpeningForEdit { get; set; } = EventCallback.Empty; /// /// Adds a contract to the list. /// /// The contract to add. - public void Add(Contract contract) + public void AddOrUpdate(Contract contract) { - _dataFetcher.Data?.Add(contract); + if (_dataFetcher.Data is null) + return; + + if (_dataFetcher.Data.Any(other => other.Id == contract.Id)) + { + _dataFetcher.Data.RemoveAll(toRemove => toRemove.Id == contract.Id); + _dataFetcher.Data.Add(contract); + } + else + { + _dataFetcher.Data.Add(contract); + } InvokeAsync(StateHasChanged); } + private FetchData> _dataFetcher = null!; + /// /// Removes a contract from the list. /// diff --git a/src/Client/Pages/Admin/ContractTableRow.razor b/src/Client/Pages/Admin/ContractTableRow.razor index e8dc756a..2ba10805 100644 --- a/src/Client/Pages/Admin/ContractTableRow.razor +++ b/src/Client/Pages/Admin/ContractTableRow.razor @@ -7,29 +7,45 @@ @Contract.SupplierName + + + + - - + @code { + /// /// Called when a contract has been removed successfully. /// [Parameter] public EventCallback OnContractRemoved { get; set; } = EventCallback.Empty; + /// + /// Called when a is being opened to be edited. + /// + [Parameter] + public EventCallback ContractOpeningForEdit { get; set; } = EventCallback.Empty; /// /// The contract. diff --git a/src/Client/wwwroot/css/app.css b/src/Client/wwwroot/css/app.css index 7149f012..e6510f68 100644 --- a/src/Client/wwwroot/css/app.css +++ b/src/Client/wwwroot/css/app.css @@ -23,6 +23,10 @@ p { line-height: 1.8125rem; } +h1, h2, h3, h4, h5, h6 { + scroll-margin-top: 4.5rem; +} + h1, h2 { font-family: 'Merriweather', Helvetica, Arial, sans-serif; letter-spacing: 0; @@ -103,12 +107,12 @@ a, .btn-link { z-index: 1000; } - #blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 0.75rem; - top: 0.5rem; - } +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} .blazor-error-boundary { background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; @@ -116,9 +120,9 @@ a, .btn-link { color: white; } - .blazor-error-boundary::after { - content: "An error has occurred." - } +.blazor-error-boundary::after { + content: "An error has occurred." +} table { diff --git a/src/Client/wwwroot/js/interop.js b/src/Client/wwwroot/js/interop.js index d3c1735c..33faddee 100644 --- a/src/Client/wwwroot/js/interop.js +++ b/src/Client/wwwroot/js/interop.js @@ -17,3 +17,7 @@ function registerActivityCallback(dotNetHelper) { dotNetHelper.invokeMethodAsync("ResetTimer"); } } + +function scrollToElement(selector) { + document.querySelector(selector).scrollIntoView({behavior: 'smooth', block: 'start'}); +} diff --git a/src/Server/Controllers/ContractsController.cs b/src/Server/Controllers/ContractsController.cs index 5885a6e1..3c4c8cb1 100644 --- a/src/Server/Controllers/ContractsController.cs +++ b/src/Server/Controllers/ContractsController.cs @@ -82,22 +82,22 @@ public IActionResult AddRecent(Contract contract) /// /// Creates a new contract. /// - /// The contract to add. + /// The contract to put. + /// The identifier of the contract to put. /// The identifier of the stored image. /// The ID of the contract was already taken. - [HttpPost] + [HttpPut("{id:guid}")] [Authorize("AdminOnly")] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public IActionResult CreateContract(Contract contract) + public IActionResult CreateContract([FromBody] Contract contract, Guid id) { try { _contracts.Add(contract); } - catch (IdentifierAlreadyTakenException e) + catch (IdentifierAlreadyTakenException) { - Logger.LogInformation("ID of contract was already taken: {Error}", e.Message); - return BadRequest(); + _contracts.UpdateContract(contract); } return Ok(); diff --git a/tests/Client.Tests/Pages/Admin/ContractListTests.cs b/tests/Client.Tests/Pages/Admin/ContractTableTests.cs similarity index 74% rename from tests/Client.Tests/Pages/Admin/ContractListTests.cs rename to tests/Client.Tests/Pages/Admin/ContractTableTests.cs index e81047bc..8656476b 100644 --- a/tests/Client.Tests/Pages/Admin/ContractListTests.cs +++ b/tests/Client.Tests/Pages/Admin/ContractTableTests.cs @@ -13,7 +13,7 @@ namespace Client.Tests.Pages.Admin; -public class ContractListTests : UITestFixture +public class ContractTableTests : UITestFixture { [Fact] public void AddingContract_RendersTheNewContract() @@ -30,7 +30,7 @@ public void AddingContract_RendersTheNewContract() var newContract = new Contract { Name = newContractName, }; // Act - cut.Instance.Add(newContract); + cut.Instance.AddOrUpdate(newContract); cut.WaitForState(() => cut.FindAll(itemSelector).Count == 3); // Assert @@ -55,7 +55,7 @@ public void AddingContract_DoesNotThrow_BeforeContractsAreFetched() var newContract = new Contract { Name = newContractName, }; // Act - Action add = () => cut.Instance.Add(newContract); + Action add = () => cut.Instance.AddOrUpdate(newContract); // Assert add.Should().NotThrow(); @@ -68,7 +68,8 @@ public async Task RemovingContract_RendersWithoutTheContractAsync() var firstContract = new Contract() { Name = "first", }; Contract[] contracts = { firstContract, new Contract() { Name = "Second", }, }; MockHttp.When("/api/v1/contracts").RespondJson(contracts); - MockHttp.When(HttpMethod.Delete, $"/api/v1/contracts/{firstContract.Id}").Respond(req => new HttpResponseMessage(HttpStatusCode.OK)); + MockHttp.When(HttpMethod.Delete, $"/api/v1/contracts/{firstContract.Id}") + .Respond(req => new HttpResponseMessage(HttpStatusCode.OK)); IRenderedComponent cut = Context.RenderComponent(); const string removeButton = "#confirm-remove"; @@ -83,4 +84,25 @@ public async Task RemovingContract_RendersWithoutTheContractAsync() elementWithNewName = contract => contract.TextContent.Contains(firstContract.Name); cut.FindAll(".contract-table-row").Should().NotContain(elementWithNewName); } + + [Fact] + public void UpdatingContract_RendersNewContent_WhenContractExists() + { + // Arrange + var contract = new Contract() { Name = "old name", }; + Contract[] contracts = { contract, }; + MockHttp.When("/api/v1/contracts").RespondJson(contracts); + + IRenderedComponent cut = Context.RenderComponent(); + cut.WaitForAssertion(() => cut.Markup.Should().Contain(contract.Name)); + + const string newName = "new name"; + + // Act + contract.Name = newName; + cut.Instance.AddOrUpdate(contract); + + // Assert + cut.WaitForAssertion(() => cut.Markup.Should().Contain(newName)); + } } diff --git a/tests/Client.Tests/Pages/Admin/UserListTests.cs b/tests/Client.Tests/Pages/Admin/UserTableTests.cs similarity index 97% rename from tests/Client.Tests/Pages/Admin/UserListTests.cs rename to tests/Client.Tests/Pages/Admin/UserTableTests.cs index 80ec5850..a990f953 100644 --- a/tests/Client.Tests/Pages/Admin/UserListTests.cs +++ b/tests/Client.Tests/Pages/Admin/UserTableTests.cs @@ -13,7 +13,7 @@ namespace Client.Tests.Pages.Admin; -public class UserListTests : UITestFixture +public class UserTableTests : UITestFixture { [Fact] public void AddingUser_RendersTheNewUser() diff --git a/tests/Server.Tests.Integration/Authentication/AuthIntegrationTests.cs b/tests/Server.Tests.Integration/Authentication/AuthIntegrationTests.cs index 787b5c68..0903f328 100644 --- a/tests/Server.Tests.Integration/Authentication/AuthIntegrationTests.cs +++ b/tests/Server.Tests.Integration/Authentication/AuthIntegrationTests.cs @@ -1,35 +1,27 @@ using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Net.Http.Json; using System.Threading.Tasks; -using Application.Configuration; + using Application.Contracts; -using Application.Users; + using Domain.Contracts; using Domain.Users; + using FluentAssertions.Execution; namespace Server.IntegrationTests.Authentication; -public class AuthIntegrationTests : IClassFixture +public class AuthIntegrationTests : IntegrationTest { - private readonly HttpClient _client; - public AuthIntegrationTests(TestWebApplicationFactory factory) + : base(factory) { - _client = factory.CreateClient(); - - Environment.SetEnvironmentVariable( - ConfigurationKeys.AdminPassword, - "test_password", - EnvironmentVariableTarget.Process); } public static IEnumerable AdminPostApiEndpoints => new List { - new object[] { "/api/v1/contracts", JsonContent.Create(new Contract()), }, new object[] { "/api/v1/users", JsonContent.Create(new User()), }, }; @@ -37,10 +29,10 @@ public static IEnumerable AdminDeleteApiEndpoints { get { - const string contractEndpoint = "/api/v1/contracts"; var contract = new Contract(); + string contractEndpoint = $"/api/v1/contracts/{contract.Id}"; Func createContract = - async client => await client.PostAsJsonAsync(contractEndpoint, contract); + async client => await client.PutAsJsonAsync(contractEndpoint, contract); const string usersEndpoint = "/api/v1/users"; var user = new User(); @@ -48,7 +40,7 @@ public static IEnumerable AdminDeleteApiEndpoints return new List { - new object[] { contractEndpoint + $"/{contract.Id}", createContract, }, + new object[] { contractEndpoint, createContract, }, new object[] { usersEndpoint + $"/{user.Id}", createUser, }, }; } @@ -61,7 +53,7 @@ public async Task GetApiEndpoints_ReturnsUnauthorized_WhenNoTokenIsSpecifiedAsyn // Arrange // Act - HttpResponseMessage response = await _client.GetAsync(endpointUrl); + HttpResponseMessage response = await Client.GetAsync(endpointUrl); // Assert response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); @@ -76,7 +68,7 @@ public async Task SendToAdminPostApiEndpoints_ReturnsUnauthorized_WhenTokenIsNot // Arrange // Act - HttpResponseMessage response = await _client.PostAsync(endpointUrl, content); + HttpResponseMessage response = await Client.PostAsync(endpointUrl, content); // Assert response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); @@ -89,10 +81,10 @@ public async Task SendToAdminPostApiEndpoints_ReturnsForbidden_WhenUserTokenIsSp HttpContent content) { // Arrange - await ArrangeAuthenticatedUser(); + await ArrangeAuthenticatedUserAsync(); // Act - HttpResponseMessage response = await _client.PostAsync(endpointUrl, content); + HttpResponseMessage response = await Client.PostAsync(endpointUrl, content); // Assert response.StatusCode.Should().Be(HttpStatusCode.Forbidden); @@ -105,10 +97,10 @@ public async Task SendToAdminPostApiEndpoints_IsSuccessful_WhenAdminIsAuthentica HttpContent content) { // Arrange - await ArrangeAuthenticatedAdmin(); + await ArrangeAuthenticatedAdminAsync(); // Act - HttpResponseMessage response = await _client.PostAsync(endpointUrl, content); + HttpResponseMessage response = await Client.PostAsync(endpointUrl, content); // Assert response.Should().BeSuccessful(); @@ -121,11 +113,11 @@ public async Task SendToAdminDeleteApiEndpoints_ReturnsForbidden_WhenUserTokenIs Func createResource) { // Arrange - await ArrangeAuthenticatedUser(); - await createResource(_client); + await ArrangeAuthenticatedUserAsync(); + await createResource(Client); // Act - HttpResponseMessage response = await _client.DeleteAsync(endpointUrl); + HttpResponseMessage response = await Client.DeleteAsync(endpointUrl); // Assert response.StatusCode.Should().Be(HttpStatusCode.Forbidden); @@ -138,11 +130,11 @@ public async Task SendToAdminDeleteApiEndpoints_IsSuccessful_WhenAdminIsAuthenti Func createResource) { // Arrange - await ArrangeAuthenticatedAdmin(); - await createResource(_client); + await ArrangeAuthenticatedAdminAsync(); + await createResource(Client); // Act - HttpResponseMessage response = await _client.DeleteAsync(endpointUrl); + HttpResponseMessage response = await Client.DeleteAsync(endpointUrl); // Assert response.Should().BeSuccessful(); @@ -153,10 +145,10 @@ public async Task GetContractsApiEndpoint_ReturnsPreviewContent_WhenUserIsNotAut { // Arrange var contract = new Contract(); - await ArrangeResource("/api/v1/contracts", contract); + await PutResourceAsync($"/api/v1/contracts/{contract.Id}", contract); // Act - HttpResponseMessage response = await _client.GetAsync("/api/v1/contracts"); + HttpResponseMessage response = await Client.GetAsync("/api/v1/contracts"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -180,10 +172,10 @@ public async Task GetContractsApiEndpoint_DoesNotReturnConfidentialContent_WhenU { // Arrange var contract = new Contract { Instructions = "very secret usage instructions", }; - await ArrangeResource("/api/v1/contracts", contract); + await PutResourceAsync("/api/v1/contracts", contract); // Act - HttpResponseMessage response = await _client.GetAsync("/api/v1/contracts"); + HttpResponseMessage response = await Client.GetAsync("/api/v1/contracts"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -197,53 +189,16 @@ public async Task GetContractsApiEndpoint_ReturnsOkContent_WhenUserIsAuthenticat { // Arrange var contract = new Contract(); - await ArrangeResource("/api/v1/contracts", contract); + await PutResourceAsync($"/api/v1/contracts/{contract.Id}", contract); - await ArrangeAuthenticatedUser(); + await ArrangeAuthenticatedUserAsync(); // Act - HttpResponseMessage response = await _client.GetAsync("/api/v1/contracts"); + HttpResponseMessage response = await Client.GetAsync("/api/v1/contracts"); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); var contracts = await response.Content.ReadFromJsonAsync>(); contracts.Should().ContainEquivalentOf(contract); } - - private async Task ArrangeResource(string url, TResource resource) - { - await ArrangeAuthenticatedAdmin(); - await _client.PostAsJsonAsync(url, resource); - - // Log out. - _client.DefaultRequestHeaders.Authorization = null; - } - - private async Task ArrangeAuthenticatedAdmin() - { - var userInfo = new User() { Name = "admin", Password = Environment.GetEnvironmentVariable(ConfigurationKeys.AdminPassword) ?? string.Empty, }; - HttpResponseMessage authResponseMessage = await _client.PostAsJsonAsync("/api/v1/users/authenticate", userInfo); - AuthenticateResponse? authResponse = - await authResponseMessage.Content.ReadFromJsonAsync(); - _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", authResponse?.Token); - } - - private async Task ArrangeAuthenticatedUser() - { - // Arrange - authenticate as admin user. - await ArrangeAuthenticatedAdmin(); - - // Arrange - create normal user. - var user = new User(); - await _client.PostAsJsonAsync("/api/v1/users", user); - - // Arrange - authenticate as normal user. - HttpResponseMessage authResponseMessage = - await _client.PostAsJsonAsync("/api/v1/users/authenticate", user); - AuthenticateResponse? authResponse = - await authResponseMessage.Content.ReadFromJsonAsync(); - - // Swap out the admin token for a normal user token. - _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", authResponse?.Token); - } } diff --git a/tests/Server.Tests.Integration/Contracts/ContractApiIntegrationTests.cs b/tests/Server.Tests.Integration/Contracts/ContractApiIntegrationTests.cs new file mode 100644 index 00000000..9a687482 --- /dev/null +++ b/tests/Server.Tests.Integration/Contracts/ContractApiIntegrationTests.cs @@ -0,0 +1,50 @@ +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; + +using Domain.Contracts; + +namespace Server.IntegrationTests.Contracts; + +public class ContractApiIntegrationTests : IntegrationTest +{ + public ContractApiIntegrationTests(TestWebApplicationFactory factory) + : base(factory) + { + } + + [Fact] + public async Task PutContract_CreatesNewContract_WhenItDoesNotExist() + { + // Arrange + await ArrangeAuthenticatedAdminAsync(); + var newContract = new Contract(); + + // Act + HttpResponseMessage response = await Client.PutAsJsonAsync($"api/v1/contracts/{newContract.Id}", newContract); + + // Assert + response.Should().BeSuccessful(); + var contracts = await Client.GetFromJsonAsync>("api/v1/contracts"); + contracts.Should().ContainEquivalentOf(newContract); + } + + [Fact] + public async Task PutContract_UpdatesExistingContract_WhenItExists() + { + // Arrange + var contract = new Contract(); + string endpointUrl = $"api/v1/contracts/{contract.Id}"; + await PutResourceAsync(endpointUrl, contract); + await ArrangeAuthenticatedAdminAsync(); + contract.Name = "Modified name"; + + // Act + HttpResponseMessage response = await Client.PutAsJsonAsync(endpointUrl, contract); + + // Assert + response.Should().BeSuccessful(); + var contracts = await Client.GetFromJsonAsync>("api/v1/contracts"); + contracts.Should().ContainEquivalentOf(contract); + } +} diff --git a/tests/Server.Tests.Integration/IntegrationTest.cs b/tests/Server.Tests.Integration/IntegrationTest.cs new file mode 100644 index 00000000..cf2ab3be --- /dev/null +++ b/tests/Server.Tests.Integration/IntegrationTest.cs @@ -0,0 +1,76 @@ +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Threading.Tasks; + +using Application.Configuration; +using Application.Users; + +using Domain.Users; + +namespace Server.IntegrationTests; + +public class IntegrationTest : IClassFixture +{ + public IntegrationTest(TestWebApplicationFactory factory) + { + Client = factory.CreateClient(); + + Environment.SetEnvironmentVariable( + ConfigurationKeys.AdminPassword, + "test_password", + EnvironmentVariableTarget.Process); + } + + protected HttpClient Client { get; } + + protected async Task PostResourceAsync(string url, TResource resource) + { + await ArrangeAuthenticatedAdminAsync(); + await Client.PostAsJsonAsync(url, resource); + + // Log out. + Client.DefaultRequestHeaders.Authorization = null; + } + + protected async Task PutResourceAsync(string url, TResource resource) + { + await ArrangeAuthenticatedAdminAsync(); + await Client.PutAsJsonAsync(url, resource); + + // Log out. + Client.DefaultRequestHeaders.Authorization = null; + } + + protected async Task ArrangeAuthenticatedAdminAsync() + { + var userInfo = new User() + { + Name = "admin", + Password = Environment.GetEnvironmentVariable(ConfigurationKeys.AdminPassword) ?? string.Empty, + }; + HttpResponseMessage authResponseMessage = await Client.PostAsJsonAsync("/api/v1/users/authenticate", userInfo); + AuthenticateResponse? authResponse = + await authResponseMessage.Content.ReadFromJsonAsync(); + Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", authResponse?.Token); + } + + protected async Task ArrangeAuthenticatedUserAsync() + { + // Arrange - authenticate as admin user. + await ArrangeAuthenticatedAdminAsync(); + + // Arrange - create normal user. + var user = new User(); + await Client.PostAsJsonAsync("/api/v1/users", user); + + // Arrange - authenticate as normal user. + HttpResponseMessage authResponseMessage = + await Client.PostAsJsonAsync("/api/v1/users/authenticate", user); + AuthenticateResponse? authResponse = + await authResponseMessage.Content.ReadFromJsonAsync(); + + // Swap out the admin token for a normal user token. + Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", authResponse?.Token); + } +}