diff --git a/.github/workflows/AareonTechnicalTest20220120110002.yml b/.github/workflows/AareonTechnicalTest20220120110002.yml new file mode 100644 index 0000000..530309e --- /dev/null +++ b/.github/workflows/AareonTechnicalTest20220120110002.yml @@ -0,0 +1,76 @@ +name: Build and deploy .NET Core application to windows webapp AareonTechnicalTest20220120110002 with API Management Service AareonTechnicalTestapi +on: + push: + branches: + - completed_test +env: + AZURE_WEBAPP_NAME: AareonTechnicalTest20220120110002 + DOTNET_CORE_VERSION: 6.0.x + WORKING_DIRECTORY: AareonTechnicalTest + CONFIGURATION: Release + AZURE_WEBAPP_PACKAGE_PATH: AareonTechnicalTest/publish + AZURE_APIM_RESOURCE_PATH: / + AZURE_APIM_RESOURCEGROUP: AareonTechnicalTest20220120105445ResourceGroup + AZURE_APIM_SERVICENAME: AareonTechnicalTestapi + AZURE_APIM_API_ID: AareonTechnicalTest + AZURE_APIM_APPSERVICEURL: https://aareontechnicaltest20220120110002.azurewebsites.net + SWASHBUCLE_ASPNET_CORE_CLI_PACKAGE_VERSION: 5.6.3 + SWASHBUCKLE_DOTNET_CORE_VERSION: 3.1.x + API_IMPORT_SPECIFICATION_PATH: AareonTechnicalTest/publish/swagger.json + API_IMPORT_DLL: AareonTechnicalTest/publish/AareonTechnicalTest.dll + API_IMPORT_VERSION: v1 +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.DOTNET_CORE_VERSION }} + - name: Setup SwashBuckle .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.SWASHBUCKLE_DOTNET_CORE_VERSION }} + - name: Restore + run: dotnet restore ${{ env.WORKING_DIRECTORY }} + - name: Build + run: dotnet build ${{ env.WORKING_DIRECTORY }} --configuration ${{ env.CONFIGURATION }} --no-restore + - name: Test + run: dotnet test ${{ env.WORKING_DIRECTORY }} --no-build + - name: Publish + run: dotnet publish ${{ env.WORKING_DIRECTORY }} --configuration ${{ env.CONFIGURATION }} --no-build --output ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + - name: Install Swashbuckle CLI .NET Global Tool + run: dotnet tool install --global Swashbuckle.AspNetCore.Cli --version ${{ env.SWASHBUCLE_ASPNET_CORE_CLI_PACKAGE_VERSION }} + working-directory: ${{ env.WORKING_DIRECTORY }} + - name: Generate Open API Specification Document + run: swagger tofile --output "${{ env.API_IMPORT_SPECIFICATION_PATH }}" "${{ env.API_IMPORT_DLL }}" "${{ env.API_IMPORT_VERSION }}" + - name: Publish Artifacts + uses: actions/upload-artifact@v1.0.0 + with: + name: webapp + path: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + deploy: + runs-on: windows-latest + needs: build + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v2 + with: + name: webapp + path: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + - name: Deploy to Azure WebApp + uses: azure/webapps-deploy@v2 + with: + app-name: ${{ env.AZURE_WEBAPP_NAME }} + package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + publish-profile: ${{ secrets.AareonTechnicalTest20220120110002_4333 }} + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AareonTechnicalTestapi_spn }} + - name: Import API into Azure API Management + run: az apim api import --path "${{ env.AZURE_APIM_RESOURCE_PATH }}" --resource-group "${{ env.AZURE_APIM_RESOURCEGROUP }}" --service-name "${{ env.AZURE_APIM_SERVICENAME }}" --api-id "${{ env.AZURE_APIM_API_ID }}" --service-url "${{ env.AZURE_APIM_APPSERVICEURL }}" --specification-path "${{ env.API_IMPORT_SPECIFICATION_PATH }}" --specification-format OpenApi --subscription-required false + - name: logout + run: > + az logout diff --git a/AareonTechnicalTest.Tests/AareonTechnicalTest.Tests.csproj b/AareonTechnicalTest.Tests/AareonTechnicalTest.Tests.csproj new file mode 100644 index 0000000..760d523 --- /dev/null +++ b/AareonTechnicalTest.Tests/AareonTechnicalTest.Tests.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + + + + diff --git a/AareonTechnicalTest.Tests/NotesServiceShould.cs b/AareonTechnicalTest.Tests/NotesServiceShould.cs new file mode 100644 index 0000000..d63e32b --- /dev/null +++ b/AareonTechnicalTest.Tests/NotesServiceShould.cs @@ -0,0 +1,102 @@ +using AareonTechnicalTest.Models; +using AareonTechnicalTest.Repositories; +using AareonTechnicalTest.Services; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; +using System.Threading.Tasks; + +namespace AareonTechnicalTest.Tests +{ + [TestClass] + public class NotesServiceShould + { + private INoteRepository? _noteRepository; + private NoteService? _noteService; + private IPersonRepository? _personRepository; + + [TestInitialize] + public void Initialise() + { + _noteRepository = Substitute.For(); + _personRepository = Substitute.For(); + + _noteService = new NoteService(_noteRepository, _personRepository); + } + + [TestMethod] + public async Task FailDeleteNoteWhenNoteNotFound() + { + var personId = 1; + var noteId = 1; + + Note? noteMock = null; + _noteRepository!.FindNoteAsync(noteId).Returns(noteMock); + + var response = await _noteService!.DeleteNoteAsync(personId, noteId); + var x = response + .Should() + .BeOfType(); + } + + [TestMethod] + public async Task FailDeleteNoteWhenPersonNotFound() + { + var personId = 1; + var noteId = 1; + + Note? noteMock = new Note { Id = noteId }; + _noteRepository!.FindNoteAsync(noteId).Returns(noteMock); + + Person? personMock = null; + _personRepository!.FindPersonAsync(noteId).Returns(personMock); + + var response = await _noteService!.DeleteNoteAsync(personId, noteId); + var x = response + .Should() + .BeOfType(); + } + + [TestMethod] + public async Task FailDeleteNoteWhenPersonIsNotAdmin() + { + var personId = 1; + var noteId = 1; + + Note? noteMock = new Note { Id = noteId }; + _noteRepository!.FindNoteAsync(noteId).Returns(noteMock); + + Person? personMock = new Person { Id = personId, IsAdmin = false }; + _personRepository!.FindPersonAsync(noteId).Returns(personMock); + + var response = await _noteService!.DeleteNoteAsync(personId, noteId); + var x = response + .Should() + .BeOfType(); + + var problemDetails = x.Subject.Value.Should().BeOfType().Which; + + problemDetails.Detail.Should().Be($"The person with ID {personId} is not an administrator.", because: "The person was not an administrator"); + } + + + [TestMethod] + public async Task DeleteNoteWhenPersonIsAdmin() + { + var personId = 1; + var noteId = 1; + + Note? noteMock = new Note { Id = noteId }; + _noteRepository!.FindNoteAsync(noteId).Returns(noteMock); + + Person? personMock = new Person { Id = personId, IsAdmin = true }; + _personRepository!.FindPersonAsync(noteId).Returns(personMock); + + var response = await _noteService!.DeleteNoteAsync(personId, noteId); + var x = response + .Should() + .BeOfType(); + } + } +} \ No newline at end of file diff --git a/AareonTechnicalTest.sln b/AareonTechnicalTest.sln index c3ca477..22e7dc9 100644 --- a/AareonTechnicalTest.sln +++ b/AareonTechnicalTest.sln @@ -1,9 +1,11 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31624.102 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32014.148 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AareonTechnicalTest", "AareonTechnicalTest\AareonTechnicalTest.csproj", "{1FB831BF-946F-4526-AB25-E5CAFC6A8E4F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AareonTechnicalTest", "AareonTechnicalTest\AareonTechnicalTest.csproj", "{1FB831BF-946F-4526-AB25-E5CAFC6A8E4F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AareonTechnicalTest.Tests", "AareonTechnicalTest.Tests\AareonTechnicalTest.Tests.csproj", "{E46C0B0C-69A8-432B-9ABB-086E0B62740A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +17,10 @@ Global {1FB831BF-946F-4526-AB25-E5CAFC6A8E4F}.Debug|Any CPU.Build.0 = Debug|Any CPU {1FB831BF-946F-4526-AB25-E5CAFC6A8E4F}.Release|Any CPU.ActiveCfg = Release|Any CPU {1FB831BF-946F-4526-AB25-E5CAFC6A8E4F}.Release|Any CPU.Build.0 = Release|Any CPU + {E46C0B0C-69A8-432B-9ABB-086E0B62740A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E46C0B0C-69A8-432B-9ABB-086E0B62740A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E46C0B0C-69A8-432B-9ABB-086E0B62740A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E46C0B0C-69A8-432B-9ABB-086E0B62740A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/AareonTechnicalTest/AareonTechnicalTest.csproj b/AareonTechnicalTest/AareonTechnicalTest.csproj index 601dd80..5f6c11a 100644 --- a/AareonTechnicalTest/AareonTechnicalTest.csproj +++ b/AareonTechnicalTest/AareonTechnicalTest.csproj @@ -1,18 +1,23 @@ - net5.0 + net6.0 - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + diff --git a/AareonTechnicalTest/ApplicationContext.cs b/AareonTechnicalTest/ApplicationContext.cs index 2218ef2..13f61fb 100644 --- a/AareonTechnicalTest/ApplicationContext.cs +++ b/AareonTechnicalTest/ApplicationContext.cs @@ -18,6 +18,8 @@ public ApplicationContext(DbContextOptions options) public virtual DbSet Tickets { get; set; } + public virtual DbSet Notes { get; set; } + public string DatabasePath { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) @@ -29,6 +31,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { PersonConfig.Configure(modelBuilder); TicketConfig.Configure(modelBuilder); + NoteConfig.Configure(modelBuilder); } } } diff --git a/AareonTechnicalTest/Controllers/NotesController.cs b/AareonTechnicalTest/Controllers/NotesController.cs new file mode 100644 index 0000000..c324942 --- /dev/null +++ b/AareonTechnicalTest/Controllers/NotesController.cs @@ -0,0 +1,50 @@ +using AareonTechnicalTest.Models; +using AareonTechnicalTest.Services; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace AareonTechnicalTest.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class NotesController : ControllerBase + { + private readonly INoteService _noteService; + + public NotesController(INoteService noteService) + { + _noteService = noteService; + } + + [HttpGet] + public async Task>> GetNotes() + { + return await _noteService.GetNotesAsync(); + } + + [HttpGet("{id}")] + public async Task> GetNote(int id) + { + return await _noteService.GetNoteAsync(id); + } + + [HttpPut("{id}")] + public async Task PutNote(int id, Note note) + { + return await _noteService.PutNoteAsync(id, note); + } + + [HttpPost] + public async Task> PostNote(Note note) + { + return await _noteService.PostNoteAsync(note); + } + + [HttpDelete("{id}")] + public async Task DeleteNote(int personId, int id) + { + return await _noteService.DeleteNoteAsync(personId, id); + } + } +} diff --git a/AareonTechnicalTest/Controllers/TicketsController.cs b/AareonTechnicalTest/Controllers/TicketsController.cs new file mode 100644 index 0000000..58b68c7 --- /dev/null +++ b/AareonTechnicalTest/Controllers/TicketsController.cs @@ -0,0 +1,50 @@ +using AareonTechnicalTest.Models; +using AareonTechnicalTest.Services; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace AareonTechnicalTest.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class TicketsController : ControllerBase + { + private readonly ITicketService _ticketService; + + public TicketsController(ITicketService ticketService) + { + _ticketService = ticketService; + } + + [HttpGet] + public async Task>> GetTickets() + { + return await _ticketService.GetTicketsAsync(); + } + + [HttpGet("{id}")] + public async Task> GetTicket(int id) + { + return await _ticketService.GetTicketAsync(id); + } + + [HttpPut("{id}")] + public async Task PutTicket(int id, Ticket ticket) + { + return await _ticketService.PutTicketAsync(id, ticket); + } + + [HttpPost] + public async Task> PostTicket(Ticket ticket) + { + return await _ticketService.PostTicketAsync(ticket); + } + + [HttpDelete("{id}")] + public async Task DeleteTicket(int id) + { + return await _ticketService.DeleteTicketAsync(id); + } + } +} diff --git a/AareonTechnicalTest/Migrations/20220119201548_AddNotes.Designer.cs b/AareonTechnicalTest/Migrations/20220119201548_AddNotes.Designer.cs new file mode 100644 index 0000000..68275ed --- /dev/null +++ b/AareonTechnicalTest/Migrations/20220119201548_AddNotes.Designer.cs @@ -0,0 +1,80 @@ +// +using AareonTechnicalTest; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AareonTechnicalTest.Migrations +{ + [DbContext(typeof(ApplicationContext))] + [Migration("20220119201548_AddNotes")] + partial class AddNotes + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); + + modelBuilder.Entity("AareonTechnicalTest.Models.Note", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("TicketId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("AareonTechnicalTest.Models.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Forename") + .HasColumnType("TEXT"); + + b.Property("IsAdmin") + .HasColumnType("INTEGER"); + + b.Property("Surname") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Persons"); + }); + + modelBuilder.Entity("AareonTechnicalTest.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AareonTechnicalTest/Migrations/20220119201548_AddNotes.cs b/AareonTechnicalTest/Migrations/20220119201548_AddNotes.cs new file mode 100644 index 0000000..3d96f22 --- /dev/null +++ b/AareonTechnicalTest/Migrations/20220119201548_AddNotes.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AareonTechnicalTest.Migrations +{ + public partial class AddNotes : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Notes", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Description = table.Column(type: "TEXT", nullable: true), + PersonId = table.Column(type: "INTEGER", nullable: false), + TicketId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Notes", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Notes"); + } + } +} diff --git a/AareonTechnicalTest/Migrations/ApplicationContextModelSnapshot.cs b/AareonTechnicalTest/Migrations/ApplicationContextModelSnapshot.cs index 4d48a1b..4153b28 100644 --- a/AareonTechnicalTest/Migrations/ApplicationContextModelSnapshot.cs +++ b/AareonTechnicalTest/Migrations/ApplicationContextModelSnapshot.cs @@ -1,6 +1,10 @@ // +using AareonTechnicalTest; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable namespace AareonTechnicalTest.Migrations { @@ -10,8 +14,27 @@ partial class ApplicationContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "5.0.11"); + modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); + + modelBuilder.Entity("AareonTechnicalTest.Models.Note", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("TicketId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Notes"); + }); modelBuilder.Entity("AareonTechnicalTest.Models.Person", b => { diff --git a/AareonTechnicalTest/Models/Note.cs b/AareonTechnicalTest/Models/Note.cs new file mode 100644 index 0000000..02c9e9a --- /dev/null +++ b/AareonTechnicalTest/Models/Note.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace AareonTechnicalTest.Models +{ + public class Note + { + [Key] + public int Id { get; set; } + + public string Description { get; set; } + + public int PersonId { get; set; } + + public int TicketId { get; set; } + } +} diff --git a/AareonTechnicalTest/Models/NoteConfig.cs b/AareonTechnicalTest/Models/NoteConfig.cs new file mode 100644 index 0000000..356662c --- /dev/null +++ b/AareonTechnicalTest/Models/NoteConfig.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; + +namespace AareonTechnicalTest.Models +{ + public static class NoteConfig + { + public static void Configure(ModelBuilder modelBuilder) + { + modelBuilder.Entity( + entity => + { + entity.HasKey(e => e.Id); + }); + + modelBuilder.Entity( + entity => + { + entity.HasKey(e => e.Id); + }); + } + } +} \ No newline at end of file diff --git a/AareonTechnicalTest/Models/Person.cs b/AareonTechnicalTest/Models/Person.cs index 3797241..50a846c 100644 --- a/AareonTechnicalTest/Models/Person.cs +++ b/AareonTechnicalTest/Models/Person.cs @@ -5,7 +5,7 @@ namespace AareonTechnicalTest.Models public class Person { [Key] - public int Id { get; } + public int Id { get; set; } public string Forename { get; set; } diff --git a/AareonTechnicalTest/Models/PersonConfig.cs b/AareonTechnicalTest/Models/PersonConfig.cs index a2ed728..602858d 100644 --- a/AareonTechnicalTest/Models/PersonConfig.cs +++ b/AareonTechnicalTest/Models/PersonConfig.cs @@ -11,6 +11,12 @@ public static void Configure(ModelBuilder modelBuilder) { entity.HasKey(e => e.Id); }); + + modelBuilder.Entity( + entity => + { + entity.HasKey(e => e.Id); + }); } } } \ No newline at end of file diff --git a/AareonTechnicalTest/Models/TicketConfig.cs b/AareonTechnicalTest/Models/TicketConfig.cs index 0f5b6b0..25bc4c2 100644 --- a/AareonTechnicalTest/Models/TicketConfig.cs +++ b/AareonTechnicalTest/Models/TicketConfig.cs @@ -11,6 +11,12 @@ public static void Configure(ModelBuilder modelBuilder) { entity.HasKey(e => e.Id); }); + + modelBuilder.Entity( + entity => + { + entity.HasKey(e => e.Id); + }); } } } diff --git a/AareonTechnicalTest/Properties/ServiceDependencies/AareonTechnicalTest20220120110002/apis1.arm.json b/AareonTechnicalTest/Properties/ServiceDependencies/AareonTechnicalTest20220120110002/apis1.arm.json new file mode 100644 index 0000000..c77c522 --- /dev/null +++ b/AareonTechnicalTest/Properties/ServiceDependencies/AareonTechnicalTest20220120110002/apis1.arm.json @@ -0,0 +1,131 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceGroupName": { + "type": "string", + "defaultValue": "AareonTechnicalTest20220120105445ResourceGroup", + "metadata": { + "_parameterType": "resourceGroup", + "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." + } + }, + "resourceGroupLocation": { + "type": "string", + "defaultValue": "southcentralus", + "metadata": { + "_parameterType": "location", + "description": "Location of the resource group. Resource groups could have different location than resources." + } + }, + "resourceLocation": { + "type": "string", + "defaultValue": "[parameters('resourceGroupLocation')]", + "metadata": { + "_parameterType": "location", + "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." + } + } + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "name": "[parameters('resourceGroupName')]", + "location": "[parameters('resourceGroupLocation')]", + "apiVersion": "2019-10-01" + }, + { + "type": "Microsoft.Resources/deployments", + "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('AareonTechnicalTest', subscription().subscriptionId)))]", + "resourceGroup": "[parameters('resourceGroupName')]", + "apiVersion": "2019-10-01", + "dependsOn": [ + "[parameters('resourceGroupName')]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "name": "AareonTechnicalTestapi", + "type": "Microsoft.ApiManagement/service", + "location": "[parameters('resourceLocation')]", + "properties": { + "publisherEmail": "mark@mbsoftwareconsulting.co.uk", + "publisherName": "Mark Bonner", + "notificationSenderEmail": "apimgmt-noreply@mail.windowsazure.com", + "hostnameConfigurations": [ + { + "type": "Proxy", + "hostName": "aareontechnicaltestapi.azure-api.net", + "encodedCertificate": null, + "keyVaultId": null, + "certificatePassword": null, + "negotiateClientCertificate": false, + "certificate": null, + "defaultSslBinding": true + } + ], + "publicIPAddresses": null, + "privateIPAddresses": null, + "additionalLocations": null, + "virtualNetworkConfiguration": null, + "customProperties": { + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls10": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls11": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls10": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls11": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Ssl30": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Protocols.Server.Http2": "False" + }, + "virtualNetworkType": "None", + "certificates": null, + "apiVersionConstraint": { + "minApiVersion": null + } + }, + "sku": { + "name": "Consumption", + "capacity": 0 + }, + "apiVersion": "2019-12-01" + }, + { + "type": "Microsoft.ApiManagement/service/apis", + "name": "AareonTechnicalTestapi/AareonTechnicalTest", + "properties": { + "displayName": "AareonTechnicalTest", + "apiRevision": "1", + "description": null, + "subscriptionRequired": true, + "serviceUrl": null, + "path": "", + "protocols": [ + "https" + ], + "authenticationSettings": { + "oAuth2": null, + "openid": null + }, + "subscriptionKeyParameterNames": { + "header": "Ocp-Apim-Subscription-Key", + "query": "subscription-key" + }, + "isCurrent": true + }, + "apiVersion": "2019-12-01", + "dependsOn": [ + "AareonTechnicalTestapi" + ] + } + ] + } + } + } + ], + "metadata": { + "_dependencyType": "apis.azure" + } +} \ No newline at end of file diff --git a/AareonTechnicalTest/Properties/ServiceDependencies/AareonTechnicalTest202201201100021/apis1.arm.json b/AareonTechnicalTest/Properties/ServiceDependencies/AareonTechnicalTest202201201100021/apis1.arm.json new file mode 100644 index 0000000..24580ca --- /dev/null +++ b/AareonTechnicalTest/Properties/ServiceDependencies/AareonTechnicalTest202201201100021/apis1.arm.json @@ -0,0 +1,132 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceGroupName": { + "type": "string", + "defaultValue": "AareonTechnicalTest20220120105445ResourceGroup", + "metadata": { + "_parameterType": "resourceGroup", + "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." + } + }, + "resourceGroupLocation": { + "type": "string", + "defaultValue": "southcentralus", + "metadata": { + "_parameterType": "location", + "description": "Location of the resource group. Resource groups could have different location than resources." + } + }, + "resourceLocation": { + "type": "string", + "defaultValue": "[parameters('resourceGroupLocation')]", + "metadata": { + "_parameterType": "location", + "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." + } + } + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "name": "[parameters('resourceGroupName')]", + "location": "[parameters('resourceGroupLocation')]", + "apiVersion": "2019-10-01" + }, + { + "type": "Microsoft.Resources/deployments", + "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('AareonTechnicalTest', subscription().subscriptionId)))]", + "resourceGroup": "[parameters('resourceGroupName')]", + "apiVersion": "2019-10-01", + "dependsOn": [ + "[parameters('resourceGroupName')]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "name": "AareonTechnicalTestapi", + "type": "Microsoft.ApiManagement/service", + "location": "[parameters('resourceLocation')]", + "properties": { + "publisherEmail": "mark@mbsoftwareconsulting.co.uk", + "publisherName": "Mark Bonner", + "notificationSenderEmail": "apimgmt-noreply@mail.windowsazure.com", + "hostnameConfigurations": [ + { + "type": "Proxy", + "hostName": "aareontechnicaltestapi.azure-api.net", + "encodedCertificate": null, + "keyVaultId": null, + "certificatePassword": null, + "negotiateClientCertificate": false, + "certificate": null, + "defaultSslBinding": true + } + ], + "publicIPAddresses": null, + "privateIPAddresses": null, + "additionalLocations": null, + "virtualNetworkConfiguration": null, + "customProperties": { + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls10": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls11": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls10": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls11": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Ssl30": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Protocols.Server.Http2": "False" + }, + "virtualNetworkType": "None", + "certificates": null, + "enableClientCertificate": true, + "apiVersionConstraint": { + "minApiVersion": null + } + }, + "sku": { + "name": "Consumption", + "capacity": 0 + }, + "apiVersion": "2019-12-01" + }, + { + "type": "Microsoft.ApiManagement/service/apis", + "name": "AareonTechnicalTestapi/AareonTechnicalTest", + "properties": { + "displayName": "AareonTechnicalTest", + "apiRevision": "1", + "description": null, + "subscriptionRequired": true, + "serviceUrl": null, + "path": "", + "protocols": [ + "https" + ], + "authenticationSettings": { + "oAuth2": null, + "openid": null + }, + "subscriptionKeyParameterNames": { + "header": "Ocp-Apim-Subscription-Key", + "query": "subscription-key" + }, + "isCurrent": true + }, + "apiVersion": "2019-12-01", + "dependsOn": [ + "AareonTechnicalTestapi" + ] + } + ] + } + } + } + ], + "metadata": { + "_dependencyType": "apis.azure" + } +} \ No newline at end of file diff --git a/AareonTechnicalTest/Properties/serviceDependencies.AareonTechnicalTest20220120110002.json b/AareonTechnicalTest/Properties/serviceDependencies.AareonTechnicalTest20220120110002.json new file mode 100644 index 0000000..a512a76 --- /dev/null +++ b/AareonTechnicalTest/Properties/serviceDependencies.AareonTechnicalTest20220120110002.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "apis1": { + "resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/Microsoft.ApiManagement/service/AareonTechnicalTestapi/apis/AareonTechnicalTest", + "type": "apis.azure" + } + } +} \ No newline at end of file diff --git a/AareonTechnicalTest/Properties/serviceDependencies.AareonTechnicalTest202201201100021.json b/AareonTechnicalTest/Properties/serviceDependencies.AareonTechnicalTest202201201100021.json new file mode 100644 index 0000000..a512a76 --- /dev/null +++ b/AareonTechnicalTest/Properties/serviceDependencies.AareonTechnicalTest202201201100021.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "apis1": { + "resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/Microsoft.ApiManagement/service/AareonTechnicalTestapi/apis/AareonTechnicalTest", + "type": "apis.azure" + } + } +} \ No newline at end of file diff --git a/AareonTechnicalTest/Properties/serviceDependencies.json b/AareonTechnicalTest/Properties/serviceDependencies.json new file mode 100644 index 0000000..e32266d --- /dev/null +++ b/AareonTechnicalTest/Properties/serviceDependencies.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "apis1": { + "type": "apis" + } + } +} \ No newline at end of file diff --git a/AareonTechnicalTest/Repositories/INoteRepository.cs b/AareonTechnicalTest/Repositories/INoteRepository.cs new file mode 100644 index 0000000..834bc91 --- /dev/null +++ b/AareonTechnicalTest/Repositories/INoteRepository.cs @@ -0,0 +1,21 @@ +using AareonTechnicalTest.Models; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace AareonTechnicalTest.Repositories +{ + public interface INoteRepository + { + Task> LoadNotesAsync(); + + Task FindNoteAsync(int id); + + Task UpdateNoteAsync(int id, Note note); + + Task NoteExistsAsync(int id); + + Task AddNoteAsync(Note note); + + Task RemoveNoteAsync(Note note); + } +} \ No newline at end of file diff --git a/AareonTechnicalTest/Repositories/IPersonRepository.cs b/AareonTechnicalTest/Repositories/IPersonRepository.cs new file mode 100644 index 0000000..56c2137 --- /dev/null +++ b/AareonTechnicalTest/Repositories/IPersonRepository.cs @@ -0,0 +1,10 @@ +using AareonTechnicalTest.Models; +using System.Threading.Tasks; + +namespace AareonTechnicalTest.Repositories +{ + public interface IPersonRepository + { + Task FindPersonAsync(int id); + } +} \ No newline at end of file diff --git a/AareonTechnicalTest/Repositories/ITicketRepository.cs b/AareonTechnicalTest/Repositories/ITicketRepository.cs new file mode 100644 index 0000000..6137945 --- /dev/null +++ b/AareonTechnicalTest/Repositories/ITicketRepository.cs @@ -0,0 +1,21 @@ +using AareonTechnicalTest.Models; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace AareonTechnicalTest.Repositories +{ + public interface ITicketRepository + { + Task> LoadTicketsAsync(); + + Task FindTicketAsync(int id); + + Task UpdateTicketAsync(int id, Ticket ticket); + + Task TicketExistsAsync(int id); + + Task AddTicketAsync(Ticket ticket); + + Task RemoveTicketAsync(Ticket ticket); + } +} \ No newline at end of file diff --git a/AareonTechnicalTest/Repositories/NoteRepository.cs b/AareonTechnicalTest/Repositories/NoteRepository.cs new file mode 100644 index 0000000..f5c2cc6 --- /dev/null +++ b/AareonTechnicalTest/Repositories/NoteRepository.cs @@ -0,0 +1,50 @@ +using AareonTechnicalTest.Models; +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace AareonTechnicalTest.Repositories +{ + public class NoteRepository : INoteRepository + { + private readonly ApplicationContext _context; + + public NoteRepository(ApplicationContext context) + { + _context = context; + } + + public async Task> LoadNotesAsync() + { + return await _context.Notes.ToListAsync(); + } + + public async Task FindNoteAsync(int id) + { + return await _context.Notes.FindAsync(id); + } + + public async Task UpdateNoteAsync(int id, Note note) + { + _context.Entry(note).State = EntityState.Modified; + await _context.SaveChangesAsync(); + } + + public async Task NoteExistsAsync(int id) + { + return await _context.Notes.AnyAsync(e => e.Id == id); + } + + public async Task AddNoteAsync(Note note) + { + _context.Notes.Add(note); + await _context.SaveChangesAsync(); + } + + public async Task RemoveNoteAsync(Note note) + { + _context.Notes.Remove(note); + await _context.SaveChangesAsync(); + } + } +} diff --git a/AareonTechnicalTest/Repositories/PersonRepository.cs b/AareonTechnicalTest/Repositories/PersonRepository.cs new file mode 100644 index 0000000..1773ea1 --- /dev/null +++ b/AareonTechnicalTest/Repositories/PersonRepository.cs @@ -0,0 +1,20 @@ +using AareonTechnicalTest.Models; +using System.Threading.Tasks; + +namespace AareonTechnicalTest.Repositories +{ + public class PersonRepository : IPersonRepository + { + private readonly ApplicationContext _context; + + public PersonRepository(ApplicationContext context) + { + _context = context; + } + + public async Task FindPersonAsync(int id) + { + return await _context.Persons.FindAsync(id); + } + } +} diff --git a/AareonTechnicalTest/Repositories/TicketRepository.cs b/AareonTechnicalTest/Repositories/TicketRepository.cs new file mode 100644 index 0000000..8036120 --- /dev/null +++ b/AareonTechnicalTest/Repositories/TicketRepository.cs @@ -0,0 +1,50 @@ +using AareonTechnicalTest.Models; +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace AareonTechnicalTest.Repositories +{ + public class TicketRepository : ITicketRepository + { + private readonly ApplicationContext _context; + + public TicketRepository(ApplicationContext context) + { + _context = context; + } + + public async Task AddTicketAsync(Ticket ticket) + { + _context.Tickets.Add(ticket); + await _context.SaveChangesAsync(); + } + + public async Task FindTicketAsync(int id) + { + return await _context.Tickets.FindAsync(id); + } + + public async Task> LoadTicketsAsync() + { + return await _context.Tickets.ToListAsync(); + } + + public async Task RemoveTicketAsync(Ticket ticket) + { + _context.Tickets.Remove(ticket); + await _context.SaveChangesAsync(); + } + + public async Task TicketExistsAsync(int id) + { + return await _context.Tickets.AnyAsync(e => e.Id == id); + } + + public async Task UpdateTicketAsync(int id, Ticket ticket) + { + _context.Entry(ticket).State = EntityState.Modified; + await _context.SaveChangesAsync(); + } + } +} diff --git a/AareonTechnicalTest/Services/INoteService.cs b/AareonTechnicalTest/Services/INoteService.cs new file mode 100644 index 0000000..708cf0d --- /dev/null +++ b/AareonTechnicalTest/Services/INoteService.cs @@ -0,0 +1,20 @@ +using AareonTechnicalTest.Models; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace AareonTechnicalTest.Services +{ + public interface INoteService + { + Task>> GetNotesAsync(); + + Task> GetNoteAsync(int id); + + Task PutNoteAsync(int id, Note note); + + Task> PostNoteAsync(Note note); + + Task DeleteNoteAsync(int personId, int id); + } +} \ No newline at end of file diff --git a/AareonTechnicalTest/Services/ITicketService.cs b/AareonTechnicalTest/Services/ITicketService.cs new file mode 100644 index 0000000..4eb3a1c --- /dev/null +++ b/AareonTechnicalTest/Services/ITicketService.cs @@ -0,0 +1,20 @@ +using AareonTechnicalTest.Models; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace AareonTechnicalTest.Services +{ + public interface ITicketService + { + Task>> GetTicketsAsync(); + + Task> GetTicketAsync(int id); + + Task PutTicketAsync(int id, Ticket ticket); + + Task> PostTicketAsync(Ticket ticket); + + Task DeleteTicketAsync(int id); + } +} \ No newline at end of file diff --git a/AareonTechnicalTest/Services/NoteService.cs b/AareonTechnicalTest/Services/NoteService.cs new file mode 100644 index 0000000..6f37d3e --- /dev/null +++ b/AareonTechnicalTest/Services/NoteService.cs @@ -0,0 +1,130 @@ +using AareonTechnicalTest.Models; +using AareonTechnicalTest.Repositories; +using Audit.Core; +using Microsoft.AspNetCore.Mvc; +using StackExchange.Profiling; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace AareonTechnicalTest.Services +{ + public class NoteService : INoteService + { + private readonly INoteRepository _noteRepository; + private readonly IPersonRepository _personRepository; + + public NoteService(INoteRepository noteRepository, IPersonRepository personRepository) + { + _noteRepository = noteRepository; + _personRepository = personRepository; + } + + public async Task>> GetNotesAsync() + { + using (MiniProfiler.Current.Step(nameof(GetNotesAsync))) + { + var notes = await _noteRepository.LoadNotesAsync(); + var scope = AuditScope.Create($"NoteService:{GetNotesAsync}", () => notes); + + return new OkObjectResult(notes); + } + } + + public async Task> GetNoteAsync(int id) + { + using (MiniProfiler.Current.Step(nameof(GetNoteAsync))) + { + var note = await _noteRepository.FindNoteAsync(id); + var scope = AuditScope.Create($"Note:{GetNoteAsync}", () => note); + + if (note is null) + { + return new NotFoundResult(); + } + + return note; + } + } + + public async Task PutNoteAsync(int id, Note note) + { + using (MiniProfiler.Current.Step(nameof(PutNoteAsync))) + { + if (id != note.Id) + { + return new BadRequestResult(); + } + + try + { + await _noteRepository.UpdateNoteAsync(id, note); + var scope = AuditScope.Create($"Note:{PutNoteAsync}", () => note); + } + catch (Exception ex) + { + if (!await _noteRepository.NoteExistsAsync(id)) + { + return new NotFoundResult(); + } + else + { + return new UnprocessableEntityObjectResult(new ProblemDetails + { + Title = "Note update could not be processed.", + Detail = $"{ex.Message}" + }); + } + } + + return new NoContentResult(); + } + } + + public async Task> PostNoteAsync(Note note) + { + using (MiniProfiler.Current.Step(nameof(PostNoteAsync))) + { + await _noteRepository.AddNoteAsync(note); + var scope = AuditScope.Create($"Note:{PostNoteAsync}", () => note); + + return new OkObjectResult(note); + } + } + + public async Task DeleteNoteAsync(int personId, int id) + { + using (MiniProfiler.Current.Step(nameof(DeleteNoteAsync))) + { + var note = await _noteRepository.FindNoteAsync(id); + if (note is null) + { + return new NotFoundResult(); + } + + var person = await _personRepository.FindPersonAsync(personId); + if (person is null) + { + return new UnprocessableEntityObjectResult(new ProblemDetails + { + Title = "Note deletion could not be processed.", + Detail = $"The person with ID {personId} could not be found." + }); + } + else if (!person.IsAdmin) + { + return new UnprocessableEntityObjectResult(new ProblemDetails + { + Title = "Note deletion could not be processed.", + Detail = $"The person with ID {personId} is not an administrator." + }); + } + + await _noteRepository.RemoveNoteAsync(note); + var scope = AuditScope.Create($"Note:{DeleteNoteAsync}", () => note); + + return new NoContentResult(); + } + } + } +} diff --git a/AareonTechnicalTest/Services/TicketService.cs b/AareonTechnicalTest/Services/TicketService.cs new file mode 100644 index 0000000..8f911d6 --- /dev/null +++ b/AareonTechnicalTest/Services/TicketService.cs @@ -0,0 +1,110 @@ +using AareonTechnicalTest.Models; +using AareonTechnicalTest.Repositories; +using Audit.Core; +using Microsoft.AspNetCore.Mvc; +using StackExchange.Profiling; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace AareonTechnicalTest.Services +{ + public class TicketService : ITicketService + { + private readonly ITicketRepository _ticketRepository; + + public TicketService(ITicketRepository ticketRepository) + { + _ticketRepository = ticketRepository; + } + + public async Task>> GetTicketsAsync() + { + using (MiniProfiler.Current.Step(nameof(GetTicketsAsync))) + { + var tickets = await _ticketRepository.LoadTicketsAsync(); + var scope = AuditScope.Create($"Ticket:{GetTicketsAsync}", () => tickets); + + return new OkObjectResult(tickets); + } + } + + public async Task> GetTicketAsync(int id) + { + using (MiniProfiler.Current.Step(nameof(GetTicketAsync))) + { + var note = await _ticketRepository.FindTicketAsync(id); + var scope = AuditScope.Create($"Ticket:{GetTicketAsync}", () => note); + + if (note is null) + { + return new NotFoundResult(); + } + + return note; + } + } + + public async Task PutTicketAsync(int id, Ticket ticket) + { + using (MiniProfiler.Current.Step(nameof(PutTicketAsync))) + { + if (id != ticket.Id) + { + return new BadRequestResult(); + } + + try + { + await _ticketRepository.UpdateTicketAsync(id, ticket); + var scope = AuditScope.Create($"Ticket:{PutTicketAsync}", () => ticket); + } + catch (Exception ex) + { + if (!await _ticketRepository.TicketExistsAsync(id)) + { + return new NotFoundResult(); + } + else + { + return new UnprocessableEntityObjectResult(new ProblemDetails + { + Title = "Ticket update could not be processed.", + Detail = $"{ex.Message}" + }); + } + } + + return new NoContentResult(); + } + } + + public async Task> PostTicketAsync(Ticket ticket) + { + using (MiniProfiler.Current.Step(nameof(PostTicketAsync))) + { + await _ticketRepository.AddTicketAsync(ticket); + var scope = AuditScope.Create($"Ticket:{PostTicketAsync}", () => ticket); + + return new OkObjectResult(ticket); + } + } + + public async Task DeleteTicketAsync(int id) + { + using (MiniProfiler.Current.Step(nameof(DeleteTicketAsync))) + { + var ticket = await _ticketRepository.FindTicketAsync(id); + if (ticket is null) + { + return new NotFoundResult(); + } + + await _ticketRepository.RemoveTicketAsync(ticket); + var scope = AuditScope.Create($"Ticket:{DeleteTicketAsync}", () => ticket); + + return new NoContentResult(); + } + } + } +} diff --git a/AareonTechnicalTest/Startup.cs b/AareonTechnicalTest/Startup.cs index 7b12082..c1cf75c 100644 --- a/AareonTechnicalTest/Startup.cs +++ b/AareonTechnicalTest/Startup.cs @@ -1,3 +1,5 @@ +using AareonTechnicalTest.Repositories; +using AareonTechnicalTest.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; @@ -20,6 +22,11 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddControllers(); services.AddDbContext(c => c.UseSqlite()); @@ -39,6 +46,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "AareonTechnicalTest v1")); } + app.UseMiniProfiler(); + app.UseHttpsRedirection(); app.UseRouting(); diff --git a/AareonTechnicalTest/Ticketing.db b/AareonTechnicalTest/Ticketing.db index 63aad03..8b16d79 100644 Binary files a/AareonTechnicalTest/Ticketing.db and b/AareonTechnicalTest/Ticketing.db differ