├── .github └── workflows │ └── main.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── CommandAPISolution.sln ├── azure-pipelines.yml ├── src └── CommandAPI │ ├── CommandAPI.csproj │ ├── Controllers │ └── CommandsController.cs │ ├── Data │ ├── CommandContext.cs │ ├── ICommandAPIRepo.cs │ ├── MockCommandAPIRepo.cs │ └── SqlCommandAPIRepo.cs │ ├── Dtos │ ├── CommandCreateDto.cs │ ├── CommandReadDto.cs │ └── CommandUpdateDto.cs │ ├── Migrations │ ├── 20200524224711_AddCommandsToDB.Designer.cs │ ├── 20200524224711_AddCommandsToDB.cs │ └── CommandContextModelSnapshot.cs │ ├── Models │ └── Command.cs │ ├── Profiles │ └── CommandsProfile.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Startup.cs │ ├── appsettings.Development.json │ └── appsettings.json └── test └── CommandAPI.Tests ├── CommandAPI.Tests.csproj ├── CommandTests.cs └── CommandsControllerTests.cs /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | 4 | pool: 5 | vmImage: 'ubuntu-latest' 6 | 7 | variables: 8 | buildConfiguration: 'Release' 9 | 10 | steps: 11 | - task: UseDotNet@2 12 | - script: dotnet build --configuration $(buildConfiguration) 13 | displayName: 'dotnet build $(buildConfiguration)' 14 | - task: DotNetCoreCLI@2 15 | displayName: 'dotnet test' 16 | inputs: 17 | command: test 18 | projects: '**/*Tests/*.csproj' 19 | testRunTitle: 'xUNit Test Run' 20 | 21 | - task: DotNetCoreCLI@2 22 | displayName: 'dotnet publish' 23 | inputs: 24 | command: publish 25 | publishWebProjects: false 26 | projects: 'src/CommandAPI/*.csproj' 27 | arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)' 28 | 29 | - task: PublishBuildArtifacts@1 30 | displayName: 'publish artifacts' 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.*~ 3 | project.lock.json 4 | .DS_Store 5 | *.pyc 6 | 7 | # Visual Studio Code 8 | .VS Code 9 | 10 | # User-specific files 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | build/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | msbuild.log 28 | msbuild.err 29 | msbuild.wrn 30 | 31 | # Visual Studio 32 | .vs/ 33 | 34 | # Compiled Source 35 | *.com 36 | *.class 37 | *.dll 38 | *.exe 39 | *.o 40 | *.so 41 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/test/CommandAPI.Tests/bin/Debug/netcoreapp3.1/CommandAPI.Tests.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}/test/CommandAPI.Tests", 15 | "console": "internalConsole", 16 | "stopAtEntry": false 17 | }, 18 | { 19 | "name": ".NET Core Attach", 20 | "type": "coreclr", 21 | "request": "attach", 22 | "processId": "${command:pickProcess}" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/test/CommandAPI.Tests/CommandAPI.Tests.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/test/CommandAPI.Tests/CommandAPI.Tests.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/test/CommandAPI.Tests/CommandAPI.Tests.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /CommandAPISolution.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B6E4FE01-9307-4F3F-BEFE-EC991E035353}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandAPI", "src\CommandAPI\CommandAPI.csproj", "{9845AF6A-A083-44AF-B483-C853467CC662}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{48FF3F06-F3B1-4E65-A2E6-A5E52999C6B7}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandAPI.Tests", "test\CommandAPI.Tests\CommandAPI.Tests.csproj", "{57755B67-D6D1-43A5-A8AF-4C41FDC598EF}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Debug|x64 = Debug|x64 18 | Debug|x86 = Debug|x86 19 | Release|Any CPU = Release|Any CPU 20 | Release|x64 = Release|x64 21 | Release|x86 = Release|x86 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {9845AF6A-A083-44AF-B483-C853467CC662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {9845AF6A-A083-44AF-B483-C853467CC662}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {9845AF6A-A083-44AF-B483-C853467CC662}.Debug|x64.ActiveCfg = Debug|Any CPU 30 | {9845AF6A-A083-44AF-B483-C853467CC662}.Debug|x64.Build.0 = Debug|Any CPU 31 | {9845AF6A-A083-44AF-B483-C853467CC662}.Debug|x86.ActiveCfg = Debug|Any CPU 32 | {9845AF6A-A083-44AF-B483-C853467CC662}.Debug|x86.Build.0 = Debug|Any CPU 33 | {9845AF6A-A083-44AF-B483-C853467CC662}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {9845AF6A-A083-44AF-B483-C853467CC662}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {9845AF6A-A083-44AF-B483-C853467CC662}.Release|x64.ActiveCfg = Release|Any CPU 36 | {9845AF6A-A083-44AF-B483-C853467CC662}.Release|x64.Build.0 = Release|Any CPU 37 | {9845AF6A-A083-44AF-B483-C853467CC662}.Release|x86.ActiveCfg = Release|Any CPU 38 | {9845AF6A-A083-44AF-B483-C853467CC662}.Release|x86.Build.0 = Release|Any CPU 39 | {57755B67-D6D1-43A5-A8AF-4C41FDC598EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {57755B67-D6D1-43A5-A8AF-4C41FDC598EF}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {57755B67-D6D1-43A5-A8AF-4C41FDC598EF}.Debug|x64.ActiveCfg = Debug|Any CPU 42 | {57755B67-D6D1-43A5-A8AF-4C41FDC598EF}.Debug|x64.Build.0 = Debug|Any CPU 43 | {57755B67-D6D1-43A5-A8AF-4C41FDC598EF}.Debug|x86.ActiveCfg = Debug|Any CPU 44 | {57755B67-D6D1-43A5-A8AF-4C41FDC598EF}.Debug|x86.Build.0 = Debug|Any CPU 45 | {57755B67-D6D1-43A5-A8AF-4C41FDC598EF}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {57755B67-D6D1-43A5-A8AF-4C41FDC598EF}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {57755B67-D6D1-43A5-A8AF-4C41FDC598EF}.Release|x64.ActiveCfg = Release|Any CPU 48 | {57755B67-D6D1-43A5-A8AF-4C41FDC598EF}.Release|x64.Build.0 = Release|Any CPU 49 | {57755B67-D6D1-43A5-A8AF-4C41FDC598EF}.Release|x86.ActiveCfg = Release|Any CPU 50 | {57755B67-D6D1-43A5-A8AF-4C41FDC598EF}.Release|x86.Build.0 = Release|Any CPU 51 | EndGlobalSection 52 | GlobalSection(NestedProjects) = preSolution 53 | {9845AF6A-A083-44AF-B483-C853467CC662} = {B6E4FE01-9307-4F3F-BEFE-EC991E035353} 54 | {57755B67-D6D1-43A5-A8AF-4C41FDC598EF} = {48FF3F06-F3B1-4E65-A2E6-A5E52999C6B7} 55 | EndGlobalSection 56 | EndGlobal 57 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | 4 | pool: 5 | vmImage: 'ubuntu-latest' 6 | 7 | variables: 8 | buildConfiguration: 'Release' 9 | 10 | steps: 11 | - task: UseDotNet@2 12 | - script: dotnet build --configuration $(buildConfiguration) 13 | displayName: 'dotnet build $(buildConfiguration)' 14 | - task: DotNetCoreCLI@2 15 | displayName: 'dotnet test' 16 | inputs: 17 | command: test 18 | projects: '**/*Tests/*.csproj' 19 | testRunTitle: 'xUNit Test Run' 20 | 21 | - task: DotNetCoreCLI@2 22 | displayName: 'dotnet publish' 23 | inputs: 24 | command: publish 25 | publishWebProjects: false 26 | projects: 'src/CommandAPI/*.csproj' 27 | arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)' 28 | 29 | - task: PublishBuildArtifacts@1 30 | displayName: 'publish artifacts' 31 | 32 | -------------------------------------------------------------------------------- /src/CommandAPI/CommandAPI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | cabd2435-bee0-47b0-8990-0889111a5d36 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/CommandAPI/Controllers/CommandsController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using AutoMapper; 3 | using CommandAPI.Dtos; 4 | using CommandAPI.Data; 5 | using CommandAPI.Models; 6 | using System.Linq; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.JsonPatch; 9 | using Microsoft.AspNetCore.Authorization; 10 | 11 | namespace CommandAPI.Controllers 12 | { 13 | [Route("api/[controller]")] 14 | [ApiController] 15 | public class CommandsController : ControllerBase 16 | { 17 | 18 | private readonly ICommandAPIRepo _repository; 19 | private readonly IMapper _mapper; 20 | 21 | public CommandsController(ICommandAPIRepo repository, IMapper mapper) 22 | { 23 | _repository = repository; 24 | _mapper = mapper; 25 | } 26 | 27 | 28 | //GET api/commands 29 | 30 | [HttpGet] 31 | public ActionResult> GetAllCommands() 32 | { 33 | var commandItems = _repository.GetAllCommands(); 34 | 35 | return Ok(_mapper.Map>(commandItems)); 36 | } 37 | 38 | //GET api/commands/{id} 39 | //[Authorize] //Apply this attribute to lockdown this ActionResult (or others) 40 | [HttpGet("{id}", Name = "GetCommandById")] 41 | public ActionResult GetCommandById(int id) 42 | { 43 | var commandItem = _repository.GetCommandById(id); 44 | if (commandItem == null) 45 | { 46 | return NotFound(); 47 | } 48 | return Ok(_mapper.Map(commandItem)); 49 | } 50 | 51 | //POST api/commands/ 52 | [HttpPost] 53 | public ActionResult CreateCommand(CommandCreateDto commandCreateDto) 54 | { 55 | var commandModel = _mapper.Map(commandCreateDto); 56 | _repository.CreateCommand(commandModel); 57 | _repository.SaveChanges(); 58 | 59 | var commandReadDto = _mapper.Map(commandModel); 60 | 61 | return CreatedAtRoute(nameof(GetCommandById), new { Id = commandReadDto.Id }, commandReadDto); 62 | } 63 | 64 | //PUT api/commands/{id} 65 | [HttpPut("{id}")] 66 | public ActionResult UpdateCommand(int id, CommandUpdateDto commandUpdateDto) 67 | { 68 | var commandModelFromRepo = _repository.GetCommandById(id); 69 | if (commandModelFromRepo == null) 70 | { 71 | return NotFound(); 72 | } 73 | _mapper.Map(commandUpdateDto, commandModelFromRepo); 74 | 75 | _repository.UpdateCommand(commandModelFromRepo); 76 | 77 | _repository.SaveChanges(); 78 | 79 | return NoContent(); 80 | } 81 | 82 | //PATCH api/commands/{id} 83 | [HttpPatch("{id}")] 84 | public ActionResult PartialCommandUpdate(int id, JsonPatchDocument patchDoc) 85 | { 86 | var commandModelFromRepo = _repository.GetCommandById(id); 87 | if(commandModelFromRepo == null) 88 | { 89 | return NotFound(); 90 | } 91 | 92 | var commandToPatch = _mapper.Map(commandModelFromRepo); 93 | patchDoc.ApplyTo(commandToPatch, ModelState); 94 | 95 | if(!TryValidateModel(commandToPatch)) 96 | { 97 | return ValidationProblem(ModelState); 98 | } 99 | 100 | _mapper.Map(commandToPatch, commandModelFromRepo); 101 | 102 | _repository.UpdateCommand(commandModelFromRepo); 103 | 104 | _repository.SaveChanges(); 105 | 106 | return NoContent(); 107 | } 108 | 109 | //DELETE api/commands/{id} 110 | [HttpDelete("{id}")] 111 | public ActionResult DeleteCommand(int id) 112 | { 113 | var commandModelFromRepo = _repository.GetCommandById(id); 114 | if(commandModelFromRepo == null) 115 | { 116 | return NotFound(); 117 | } 118 | _repository.DeleteCommand(commandModelFromRepo); 119 | _repository.SaveChanges(); 120 | 121 | return NoContent(); 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /src/CommandAPI/Data/CommandContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using CommandAPI.Models; 3 | 4 | namespace CommandAPI.Data 5 | { 6 | public class CommandContext : DbContext 7 | { 8 | public CommandContext(DbContextOptions options) : base(options) 9 | { 10 | 11 | } 12 | 13 | public DbSet CommandItems {get; set;} 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/CommandAPI/Data/ICommandAPIRepo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CommandAPI.Models; 3 | 4 | namespace CommandAPI.Data 5 | { 6 | public interface ICommandAPIRepo 7 | { 8 | bool SaveChanges(); 9 | 10 | IEnumerable GetAllCommands(); 11 | Command GetCommandById(int id); 12 | void CreateCommand(Command cmd); 13 | void UpdateCommand(Command cmd); 14 | void DeleteCommand(Command cmd); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/CommandAPI/Data/MockCommandAPIRepo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CommandAPI.Models; 3 | 4 | namespace CommandAPI.Data 5 | { 6 | public class MockCommandAPIRepo : ICommandAPIRepo 7 | { 8 | public void CreateCommand(Command cmd) 9 | { 10 | throw new System.NotImplementedException(); 11 | } 12 | 13 | public void DeleteCommand(Command cmd) 14 | { 15 | throw new System.NotImplementedException(); 16 | } 17 | 18 | public IEnumerable GetAllCommands() 19 | { 20 | var commands = new List 21 | { 22 | new Command{ 23 | Id=0, HowTo="How to genrate a migration", 24 | CommandLine="dotnet ef migrations add ", 25 | Platform=".Net Core EF"}, 26 | new Command{ 27 | Id=1, HowTo="Run Migrations", 28 | CommandLine="dotnet ef database update", 29 | Platform=".Net Core EF"}, 30 | new Command{ 31 | Id=2, HowTo="List active migrations", 32 | CommandLine="dotnet ef migrations list", 33 | Platform=".Net Core EF"} 34 | }; 35 | 36 | return commands; 37 | } 38 | 39 | public Command GetCommandById(int id) 40 | { 41 | return new Command{ 42 | Id=0, HowTo="How to genrate a migration", 43 | CommandLine="dotnet ef migrations add ", 44 | Platform=".Net Core EF"}; 45 | } 46 | 47 | public bool SaveChanges() 48 | { 49 | throw new System.NotImplementedException(); 50 | } 51 | 52 | public void UpdateCommand(Command cmd) 53 | { 54 | throw new System.NotImplementedException(); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/CommandAPI/Data/SqlCommandAPIRepo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using CommandAPI.Models; 5 | 6 | namespace CommandAPI.Data 7 | { 8 | public class SqlCommandAPIRepo : ICommandAPIRepo 9 | { 10 | private readonly CommandContext _context; 11 | 12 | public SqlCommandAPIRepo(CommandContext context) 13 | { 14 | _context = context; 15 | } 16 | 17 | public void CreateCommand(Command cmd) 18 | { 19 | if(cmd == null) 20 | { 21 | throw new ArgumentNullException(nameof(cmd)); 22 | } 23 | 24 | _context.CommandItems.Add(cmd); 25 | } 26 | 27 | public void DeleteCommand(Command cmd) 28 | { 29 | if(cmd == null) 30 | { 31 | throw new ArgumentNullException(nameof(cmd)); 32 | } 33 | _context.CommandItems.Remove(cmd); 34 | } 35 | 36 | public IEnumerable GetAllCommands() 37 | { 38 | return _context.CommandItems.ToList(); 39 | } 40 | 41 | public Command GetCommandById(int id) 42 | { 43 | return _context.CommandItems.FirstOrDefault(p => p.Id == id); 44 | } 45 | 46 | public bool SaveChanges() 47 | { 48 | return (_context.SaveChanges() >= 0); 49 | } 50 | 51 | public void UpdateCommand(Command cmd) 52 | { 53 | //This does nothing 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/CommandAPI/Dtos/CommandCreateDto.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace CommandAPI.Dtos 4 | { 5 | public class CommandCreateDto 6 | { 7 | [Required] 8 | [MaxLength(250)] 9 | public string HowTo {get; set;} 10 | 11 | [Required] 12 | public string Platform {get; set;} 13 | 14 | [Required] 15 | public string CommandLine {get; set;} 16 | } 17 | } -------------------------------------------------------------------------------- /src/CommandAPI/Dtos/CommandReadDto.cs: -------------------------------------------------------------------------------- 1 | namespace CommandAPI.Dtos 2 | { 3 | public class CommandReadDto 4 | { 5 | public int Id {get; set;} 6 | 7 | public string HowTo {get; set;} 8 | 9 | public string Platform {get; set;} 10 | 11 | public string CommandLine {get; set;} 12 | } 13 | } -------------------------------------------------------------------------------- /src/CommandAPI/Dtos/CommandUpdateDto.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace CommandAPI.Dtos 4 | { 5 | public class CommandUpdateDto 6 | { 7 | [Required] 8 | [MaxLength(250)] 9 | public string HowTo {get; set;} 10 | 11 | [Required] 12 | public string Platform {get; set;} 13 | 14 | [Required] 15 | public string CommandLine {get; set;} 16 | } 17 | } -------------------------------------------------------------------------------- /src/CommandAPI/Migrations/20200524224711_AddCommandsToDB.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using CommandAPI.Data; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 8 | 9 | namespace CommandAPI.Migrations 10 | { 11 | [DbContext(typeof(CommandContext))] 12 | [Migration("20200524224711_AddCommandsToDB")] 13 | partial class AddCommandsToDB 14 | { 15 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) 20 | .HasAnnotation("ProductVersion", "3.1.4") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 22 | 23 | modelBuilder.Entity("CommandAPI.Models.Command", b => 24 | { 25 | b.Property("Id") 26 | .ValueGeneratedOnAdd() 27 | .HasColumnType("integer") 28 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); 29 | 30 | b.Property("CommandLine") 31 | .IsRequired() 32 | .HasColumnType("text"); 33 | 34 | b.Property("HowTo") 35 | .IsRequired() 36 | .HasColumnType("character varying(250)") 37 | .HasMaxLength(250); 38 | 39 | b.Property("Platform") 40 | .IsRequired() 41 | .HasColumnType("text"); 42 | 43 | b.HasKey("Id"); 44 | 45 | b.ToTable("CommandItems"); 46 | }); 47 | #pragma warning restore 612, 618 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/CommandAPI/Migrations/20200524224711_AddCommandsToDB.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 3 | 4 | namespace CommandAPI.Migrations 5 | { 6 | public partial class AddCommandsToDB : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "CommandItems", 12 | columns: table => new 13 | { 14 | Id = table.Column(nullable: false) 15 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), 16 | HowTo = table.Column(maxLength: 250, nullable: false), 17 | Platform = table.Column(nullable: false), 18 | CommandLine = table.Column(nullable: false) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_CommandItems", x => x.Id); 23 | }); 24 | } 25 | 26 | protected override void Down(MigrationBuilder migrationBuilder) 27 | { 28 | migrationBuilder.DropTable( 29 | name: "CommandItems"); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/CommandAPI/Migrations/CommandContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using CommandAPI.Data; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 7 | 8 | namespace CommandAPI.Migrations 9 | { 10 | [DbContext(typeof(CommandContext))] 11 | partial class CommandContextModelSnapshot : ModelSnapshot 12 | { 13 | protected override void BuildModel(ModelBuilder modelBuilder) 14 | { 15 | #pragma warning disable 612, 618 16 | modelBuilder 17 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) 18 | .HasAnnotation("ProductVersion", "3.1.4") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 20 | 21 | modelBuilder.Entity("CommandAPI.Models.Command", b => 22 | { 23 | b.Property("Id") 24 | .ValueGeneratedOnAdd() 25 | .HasColumnType("integer") 26 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); 27 | 28 | b.Property("CommandLine") 29 | .IsRequired() 30 | .HasColumnType("text"); 31 | 32 | b.Property("HowTo") 33 | .IsRequired() 34 | .HasColumnType("character varying(250)") 35 | .HasMaxLength(250); 36 | 37 | b.Property("Platform") 38 | .IsRequired() 39 | .HasColumnType("text"); 40 | 41 | b.HasKey("Id"); 42 | 43 | b.ToTable("CommandItems"); 44 | }); 45 | #pragma warning restore 612, 618 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/CommandAPI/Models/Command.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace CommandAPI.Models 4 | { 5 | public class Command 6 | { 7 | [Key] 8 | [Required] 9 | public int Id {get; set;} 10 | 11 | [Required] 12 | [MaxLength(250)] 13 | public string HowTo {get; set;} 14 | 15 | [Required] 16 | public string Platform {get; set;} 17 | 18 | [Required] 19 | public string CommandLine {get; set;} 20 | } 21 | } -------------------------------------------------------------------------------- /src/CommandAPI/Profiles/CommandsProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CommandAPI.Dtos; 3 | using CommandAPI.Models; 4 | 5 | namespace CommandAPI.Profiles 6 | { 7 | public class CommandsProfile : Profile 8 | { 9 | public CommandsProfile() 10 | { 11 | //Source -> Target 12 | CreateMap(); 13 | CreateMap(); 14 | CreateMap(); 15 | CreateMap(); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/CommandAPI/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace CommandAPI 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/CommandAPI/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:10197", 7 | "sslPort": 44339 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "CommandAPI": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/CommandAPI/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CommandAPI.Data; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Hosting; 12 | using Microsoft.EntityFrameworkCore; 13 | using Npgsql; 14 | using AutoMapper; 15 | using Newtonsoft.Json.Serialization; 16 | using Microsoft.AspNetCore.Authentication.JwtBearer; 17 | 18 | namespace CommandAPI 19 | { 20 | public class Startup 21 | { 22 | public IConfiguration Configuration {get;} 23 | public Startup(IConfiguration configuration) 24 | { 25 | Configuration = configuration; 26 | } 27 | 28 | public void ConfigureServices(IServiceCollection services) 29 | { 30 | var builder = new NpgsqlConnectionStringBuilder(); 31 | builder.ConnectionString = 32 | Configuration.GetConnectionString("PostgreSqlConnection"); 33 | builder.Username = Configuration["UserID"]; 34 | builder.Password = Configuration["Password"]; 35 | 36 | services.AddDbContext(opt => opt.UseNpgsql(builder.ConnectionString)); 37 | 38 | services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(opt => 39 | { 40 | opt.Audience = Configuration["ResourceID"]; 41 | opt.Authority = $"{Configuration["Instance"]}{Configuration["TenantId"]}"; 42 | }); 43 | 44 | services.AddControllers().AddNewtonsoftJson(s => { 45 | s.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); 46 | }); 47 | 48 | services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); 49 | 50 | services.AddScoped(); 51 | } 52 | 53 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env, CommandContext context) 54 | { 55 | context.Database.Migrate(); 56 | if (env.IsDevelopment()) 57 | { 58 | app.UseDeveloperExceptionPage(); 59 | } 60 | 61 | app.UseRouting(); 62 | 63 | app.UseAuthentication(); 64 | app.UseAuthorization(); 65 | 66 | app.UseEndpoints(endpoints => 67 | { 68 | endpoints.MapControllers(); 69 | }); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/CommandAPI/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "ConnectionStrings": { 10 | "PostgreSqlConnection":"Host=localhost;Port=5432;Database=CmdAPI;Pooling=true;" 11 | } 12 | } 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/CommandAPI/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } -------------------------------------------------------------------------------- /test/CommandAPI.Tests/CommandAPI.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/CommandAPI.Tests/CommandTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using CommandAPI.Models; 4 | 5 | namespace CommandAPI.Tests 6 | { 7 | public class CommandTests : IDisposable 8 | { 9 | Command testCommand; 10 | 11 | public CommandTests() 12 | { 13 | testCommand = new Command 14 | { 15 | HowTo = "Do something", 16 | Platform = "Some platform", 17 | CommandLine = "Some commandline" 18 | }; 19 | } 20 | 21 | public void Dispose() 22 | { 23 | testCommand = null; 24 | } 25 | 26 | 27 | [Fact] 28 | public void CanChangeHowTo() 29 | { 30 | //Arrange 31 | 32 | //Act 33 | testCommand.HowTo = "Execute Unit Tests"; 34 | 35 | //Assert 36 | Assert.Equal("Execute Unit Tests", testCommand.HowTo); 37 | 38 | } 39 | 40 | [Fact] 41 | public void CanChangePlatform() 42 | { 43 | //Arrange 44 | 45 | //Act 46 | testCommand.Platform = "xUnit"; 47 | 48 | //Assert 49 | Assert.Equal("xUnit", testCommand.Platform); 50 | 51 | } 52 | 53 | [Fact] 54 | public void CanChangeCommandLine() 55 | { 56 | //Arrange 57 | 58 | //Act 59 | testCommand.CommandLine = "dotnet test"; 60 | 61 | //Assert 62 | Assert.Equal("dotnet test", testCommand.CommandLine); 63 | 64 | } 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/CommandAPI.Tests/CommandsControllerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Moq; 4 | using AutoMapper; 5 | using CommandAPI.Models; 6 | using Xunit; 7 | using CommandAPI.Controllers; 8 | using CommandAPI.Data; 9 | using CommandAPI.Profiles; 10 | using Microsoft.AspNetCore.Mvc; 11 | using CommandAPI.Dtos; 12 | 13 | using Newtonsoft.Json.Serialization; 14 | 15 | namespace CommandAPI.Tests 16 | { 17 | public class CommandsControllerTests : IDisposable 18 | { 19 | Mock mockRepo; 20 | CommandsProfile realProfile; 21 | MapperConfiguration configuration; 22 | IMapper mapper; 23 | 24 | public CommandsControllerTests() 25 | { 26 | mockRepo = new Mock(); 27 | realProfile = new CommandsProfile(); 28 | configuration = new MapperConfiguration(cfg => cfg.AddProfile(realProfile)); 29 | mapper = new Mapper(configuration); 30 | } 31 | 32 | public void Dispose() 33 | { 34 | mockRepo = null; 35 | mapper = null; 36 | configuration = null; 37 | realProfile = null; 38 | } 39 | 40 | 41 | 42 | //************************************************** 43 | //* 44 | //GET /api/commands Unit Tests 45 | //* 46 | //************************************************** 47 | 48 | //TEST 1.1 49 | [Fact] 50 | public void GetAllCommands_ReturnsZeroResources_WhenDBIsEmpty() 51 | { 52 | //Arrange 53 | mockRepo.Setup(repo => 54 | repo.GetAllCommands()).Returns(GetCommands(0)); 55 | 56 | var controller = new CommandsController(mockRepo.Object, mapper); 57 | 58 | //Act 59 | var result = controller.GetAllCommands(); 60 | 61 | //Assert 62 | Assert.IsType(result.Result); 63 | } 64 | 65 | //TEST 1.2 66 | [Fact] 67 | public void GetAllCommands_ReturnsOneResource_WhenDBHasOneResource() 68 | { 69 | //Arrange 70 | mockRepo.Setup(repo => 71 | repo.GetAllCommands()).Returns(GetCommands(1)); 72 | 73 | var controller = new CommandsController(mockRepo.Object, mapper); 74 | 75 | //Act 76 | var result = controller.GetAllCommands(); 77 | 78 | //Assert 79 | var okResult = result.Result as OkObjectResult; 80 | 81 | var commands = okResult.Value as List; 82 | 83 | Assert.Single(commands); 84 | } 85 | 86 | //TEST 1.3 87 | [Fact] 88 | public void GetAllCommands_Returns200OK_WhenDBHasOneResource() 89 | { 90 | //Arrange 91 | mockRepo.Setup(repo => 92 | repo.GetAllCommands()).Returns(GetCommands(1)); 93 | 94 | var controller = new CommandsController(mockRepo.Object, mapper); 95 | 96 | //Act 97 | var result = controller.GetAllCommands(); 98 | 99 | //Assert 100 | Assert.IsType(result.Result); 101 | 102 | } 103 | 104 | //TEST 1.4 105 | [Fact] 106 | public void GetAllCommands_ReturnsCorrectType_WhenDBHasOneResource() 107 | { 108 | //Arrange 109 | mockRepo.Setup(repo => 110 | repo.GetAllCommands()).Returns(GetCommands(1)); 111 | 112 | var controller = new CommandsController(mockRepo.Object, mapper); 113 | 114 | //Act 115 | var result = controller.GetAllCommands(); 116 | 117 | //Assert 118 | Assert.IsType>>(result); 119 | } 120 | 121 | //************************************************** 122 | //* 123 | //GET /api/commands/{id} Unit Tests 124 | //* 125 | //************************************************** 126 | 127 | //TEST 2.1 128 | [Fact] 129 | public void GetCommandByID_Returns404NotFound_WhenNonExistentIDProvided() 130 | { 131 | //Arrange 132 | mockRepo.Setup(repo => 133 | repo.GetCommandById(0)).Returns(() => null); 134 | 135 | var controller = new CommandsController(mockRepo.Object, mapper); 136 | 137 | //Act 138 | var result = controller.GetCommandById(1); 139 | 140 | //Assert 141 | Assert.IsType(result.Result); 142 | } 143 | 144 | //TEST 2.2 145 | [Fact] 146 | public void GetCommandByID_Returns200OK__WhenValidIDProvided() 147 | { 148 | //Arrange 149 | mockRepo.Setup(repo => 150 | repo.GetCommandById(1)).Returns(new Command { Id = 1, HowTo = "mock", Platform = "Mock", CommandLine = "Mock" }); 151 | 152 | var controller = new CommandsController(mockRepo.Object, mapper); 153 | 154 | //Act 155 | var result = controller.GetCommandById(1); 156 | 157 | //Assert 158 | Assert.IsType(result.Result); 159 | } 160 | 161 | //TEST 2.3 162 | [Fact] 163 | public void GetCommandByID_ReturnsCorrectResouceType_WhenValidIDProvided() 164 | { 165 | //Arrange 166 | mockRepo.Setup(repo => 167 | repo.GetCommandById(1)).Returns(new Command { Id = 1, HowTo = "mock", Platform = "Mock", CommandLine = "Mock" }); 168 | 169 | var controller = new CommandsController(mockRepo.Object, mapper); 170 | 171 | //Act 172 | var result = controller.GetCommandById(1); 173 | 174 | //Assert 175 | Assert.IsType>(result); 176 | } 177 | 178 | //************************************************** 179 | //* 180 | //POST /api/commands/ Unit Tests 181 | //* 182 | //************************************************** 183 | 184 | //TEST 3.1 185 | [Fact] 186 | public void CreateCommand_ReturnsCorrectResourceType_WhenValidObjectSubmitted() 187 | { 188 | //Arrange 189 | mockRepo.Setup(repo => 190 | repo.GetCommandById(1)).Returns(new Command { Id = 1, HowTo = "mock", Platform = "Mock", CommandLine = "Mock" }); 191 | 192 | var controller = new CommandsController(mockRepo.Object, mapper); 193 | 194 | //Act 195 | var result = controller.CreateCommand(new CommandCreateDto { }); 196 | 197 | //Assert 198 | Assert.IsType>(result); 199 | } 200 | 201 | //TEST 3.2 202 | [Fact] 203 | public void CreateCommand_Returns201Created_WhenValidObjectSubmitted() 204 | { 205 | //Arrange 206 | mockRepo.Setup(repo => 207 | repo.GetCommandById(1)).Returns(new Command { Id = 1, HowTo = "mock", Platform = "Mock", CommandLine = "Mock" }); 208 | 209 | var controller = new CommandsController(mockRepo.Object, mapper); 210 | 211 | //Act 212 | var result = controller.CreateCommand(new CommandCreateDto { }); 213 | 214 | //Assert 215 | Assert.IsType(result.Result); 216 | } 217 | 218 | 219 | //************************************************** 220 | //* 221 | //PUT /api/commands/{id} Unit Tests 222 | //* 223 | //************************************************** 224 | 225 | //TEST 4.1 226 | [Fact] 227 | public void UpdateCommand_Returns204NoContent_WhenValidObjectSubmitted() 228 | { 229 | //Arrange 230 | mockRepo.Setup(repo => 231 | repo.GetCommandById(1)).Returns(new Command { Id = 1, HowTo = "mock", Platform = "Mock", CommandLine = "Mock" }); 232 | 233 | var controller = new CommandsController(mockRepo.Object, mapper); 234 | 235 | //Act 236 | var result = controller.UpdateCommand(1, new CommandUpdateDto { }); 237 | 238 | //Assert 239 | Assert.IsType(result); 240 | } 241 | 242 | 243 | //TEST 4.2 244 | [Fact] 245 | public void UpdateCommand_Returns404NotFound_WhenNonExistentResourceIDSubmitted() 246 | { 247 | //Arrange 248 | mockRepo.Setup(repo => 249 | repo.GetCommandById(0)).Returns(() => null); 250 | 251 | var controller = new CommandsController(mockRepo.Object, mapper); 252 | 253 | //Act 254 | var result = controller.UpdateCommand(0, new CommandUpdateDto { }); 255 | 256 | //Assert 257 | Assert.IsType(result); 258 | } 259 | 260 | 261 | //************************************************** 262 | //* 263 | //PATCH /api/commands/{id} Unit Tests 264 | //* 265 | //************************************************** 266 | 267 | 268 | //TEST 5.1 269 | [Fact] 270 | public void PartialCommandUpdate_Returns404NotFound_WhenNonExistentResourceIDSubmitted() 271 | { 272 | //Arrange 273 | mockRepo.Setup(repo => 274 | repo.GetCommandById(0)).Returns(() => null); 275 | 276 | var controller = new CommandsController(mockRepo.Object, mapper); 277 | 278 | //Act 279 | var result = controller.PartialCommandUpdate(0, new Microsoft.AspNetCore.JsonPatch.JsonPatchDocument { }); 280 | 281 | //Assert 282 | Assert.IsType(result); 283 | } 284 | 285 | 286 | //************************************************** 287 | //* 288 | //DELETE /api/commands/{id} Unit Tests 289 | //* 290 | //************************************************** 291 | 292 | //TEST 6.1 293 | [Fact] 294 | public void DeleteCommand_Returns200OK_WhenValidResourceIDSubmitted() 295 | { 296 | //Arrange 297 | mockRepo.Setup(repo => 298 | repo.GetCommandById(1)).Returns(new Command { Id = 1, HowTo = "mock", Platform = "Mock", CommandLine = "Mock" }); 299 | 300 | var controller = new CommandsController(mockRepo.Object, mapper); 301 | 302 | //Act 303 | var result = controller.DeleteCommand(1); 304 | 305 | //Assert 306 | Assert.IsType(result); 307 | } 308 | 309 | //TEST 6.2 310 | [Fact] 311 | public void DeleteCommand_Returns_404NotFound_WhenNonExistentResourceIDSubmitted() 312 | { 313 | //Arrange 314 | mockRepo.Setup(repo => 315 | repo.GetCommandById(0)).Returns(() => null); 316 | 317 | var controller = new CommandsController(mockRepo.Object, mapper); 318 | 319 | //Act 320 | var result = controller.DeleteCommand(0); 321 | 322 | //Assert 323 | Assert.IsType(result); 324 | 325 | } 326 | 327 | //************************************************** 328 | //* 329 | //Private Support Methods 330 | //* 331 | //************************************************** 332 | 333 | 334 | 335 | 336 | private List GetCommands(int num) 337 | { 338 | var commands = new List(); 339 | if (num > 0) 340 | { 341 | commands.Add(new Command 342 | { 343 | Id = 0, 344 | HowTo = "How to genrate a migration", 345 | CommandLine = "dotnet ef migrations add ", 346 | Platform = ".Net Core EF" 347 | }); 348 | } 349 | return commands; 350 | } 351 | } 352 | } 353 | --------------------------------------------------------------------------------