diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b3282ea5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,93 @@ +# Visual Studio +.vs/ +*.suo +*.user +*.userprefs +*.log +*.pdb +*.pidb +*.vshost.* +*.testsettings +*.trx +*.webtest +*.cache +*.obj +*.bak +*.resharper +*.dotSettings +*.resharper.user +*.resharper.host +*.csproj.user +*.vcxproj.user +*.sln.docstates +*.suo +*.user +*.userprefs +*.log +*.pdb +*.pidb +*.vshost.* +*.testsettings +*.trx +*.webtest +*.cache +*.obj +*.bak +*.resharper +*.dotSettings +*.resharper.user +*.resharper.host +*.csproj.user +*.vcxproj.user +*.sln.docstates + +# Build artifacts +[Bb]in/ +[Oo]bj/ +[Dd]ebug/ +[Rr]elease/ +*.dll +*.exe +*.exp +*.lib +*.ilk +*.idb +*.winmd +*.appx +*.msi +*.msp +*.pri +*.sgen +*.xap +*.zip +*.nupkg +*.publish +*.deploy +_PublishedWebsites/ +app.publish/ +TestResults/ + +# NuGet packages +packages/ +*.nuget.props +*.nuget.targets + +# Node.js (if used for front-end or build tools) +node_modules/ +npm-debug.log + +# Rider +.idea/ + +# Visual Studio Code +.vscode/ + +# Environment-specific files +appsettings.*.json # Exclude sensitive appsettings files, but keep appsettings.json +*.json.user +*.env +.env.* + +# Miscellaneous +Thumbs.db +Desktop.ini \ No newline at end of file diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI.IntegrationTests/IntegrationTests.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI.IntegrationTests/IntegrationTests.cs new file mode 100644 index 00000000..a27201fa --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI.IntegrationTests/IntegrationTests.cs @@ -0,0 +1,98 @@ +using InvestmentPerformanceAPI; +using InvestmentPerformanceAPI.Models; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace InvestmentPerformanceAPI.IntegrationTests +{ + public class UsersControllerTests + { + private WebApplicationFactory _factory; + private HttpClient _client; + + [OneTimeSetUp] + public void Setup() + { + _factory = new WebApplicationFactory() + .WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + services.Remove( + services.SingleOrDefault(d => d.ServiceType == typeof(IDbContextOptionsConfiguration)) + ); + + services.AddDbContext(options => + options.UseInMemoryDatabase("IntegrationTestsDb")); + + var sp = services.BuildServiceProvider(); + using var scope = sp.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + context.Database.EnsureCreated(); + + context.Users.Add(new User + { + Id = 1, + Name = "Rudy", + Investments = new List + { + new Investment { Id = 1, Name = "QQQ", CostBasisPerShare = 40, CurrentPrice = 70, NumberOfShares = 1, CurrentValue = 70 }, + new Investment { Id = 2, Name = "SPY", CostBasisPerShare = 50, CurrentPrice = 80, NumberOfShares = 2, CurrentValue = 160 } + } + }); + context.SaveChanges(); + }); + }); + + _client = _factory.CreateClient(); + } + + [OneTimeTearDown] + public void TearDown() + { + _factory.Dispose(); + _client.Dispose(); + } + + [Test] + public async Task GetUserInvestments_ReturnsOk_WhenUserExists() + { + var response = await _client.GetAsync("/user/1/investments"); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + Assert.That(json.Contains("QQQ")); + Assert.That(json.Contains("SPY")); + } + + [Test] + public async Task GetUserInvestments_ReturnsNotFound_WhenUserDoesNotExist() + { + var response = await _client.GetAsync("user/99/investments"); + Assert.That(response.StatusCode, Is.EqualTo(System.Net.HttpStatusCode.NotFound)); + } + + [Test] + public async Task GetUserInvestmentDetails_ReturnsOk_WhenUserExists() + { + var response = await _client.GetAsync("/user/1/investment/2"); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + Assert.That(json.Contains("SPY")); + } + + [Test] + public async Task GetUserInvestmentDetails_ReturnsNotFound_WhenUserDoesNotExist() + { + var response = await _client.GetAsync("user/99/investment/2"); + Assert.That(response.StatusCode, Is.EqualTo(System.Net.HttpStatusCode.NotFound)); + } + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI.IntegrationTests/InvestmentPerformanceAPI.IntegrationTests.csproj b/InvestmentPerformanceAPI/InvestmentPerformanceAPI.IntegrationTests/InvestmentPerformanceAPI.IntegrationTests.csproj new file mode 100644 index 00000000..d9e2d839 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI.IntegrationTests/InvestmentPerformanceAPI.IntegrationTests.csproj @@ -0,0 +1,30 @@ + + + + net9.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI.sln b/InvestmentPerformanceAPI/InvestmentPerformanceAPI.sln new file mode 100644 index 00000000..76201684 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36705.20 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InvestmentPerformanceAPI", "InvestmentPerformanceAPI\InvestmentPerformanceAPI.csproj", "{58D4DC25-A7FF-499C-9985-CFF8F3A3E409}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InvestmentPerformanceAPI.IntegrationTests", "InvestmentPerformanceAPI.IntegrationTests\InvestmentPerformanceAPI.IntegrationTests.csproj", "{131612DC-1D45-D8C9-7123-FA23DF84F028}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {58D4DC25-A7FF-499C-9985-CFF8F3A3E409}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58D4DC25-A7FF-499C-9985-CFF8F3A3E409}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58D4DC25-A7FF-499C-9985-CFF8F3A3E409}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58D4DC25-A7FF-499C-9985-CFF8F3A3E409}.Release|Any CPU.Build.0 = Release|Any CPU + {131612DC-1D45-D8C9-7123-FA23DF84F028}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {131612DC-1D45-D8C9-7123-FA23DF84F028}.Debug|Any CPU.Build.0 = Debug|Any CPU + {131612DC-1D45-D8C9-7123-FA23DF84F028}.Release|Any CPU.ActiveCfg = Release|Any CPU + {131612DC-1D45-D8C9-7123-FA23DF84F028}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AEB35FDF-1ACA-495F-BCDC-7E80009BD3C7} + EndGlobalSection +EndGlobal diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/AppDbContext.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/AppDbContext.cs new file mode 100644 index 00000000..16a3d1bf --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/AppDbContext.cs @@ -0,0 +1,18 @@ +using InvestmentPerformanceAPI.Models; +using Microsoft.EntityFrameworkCore; + +namespace InvestmentPerformanceAPI +{ + public class AppDbContext : DbContext, IAppDbContext + { + public AppDbContext(DbContextOptions options) : base(options) { } + + public DbSet Users { get; set; } + public DbSet Investments { get; set; } + + public override Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return base.SaveChangesAsync(cancellationToken); + } + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Controllers/UserController.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Controllers/UserController.cs new file mode 100644 index 00000000..f9579b80 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Controllers/UserController.cs @@ -0,0 +1,53 @@ +using InvestmentPerformanceAPI.DTOs; +using InvestmentPerformanceAPI.Models; +using InvestmentPerformanceAPI.Services; +using Microsoft.AspNetCore.Mvc; + +namespace InvestmentPerformanceAPI.Controllers +{ + [ApiController] + [Route("[controller]")] + public class UserController : ControllerBase + { + private readonly ILogger _logger; + private readonly IUserInvestmentService _userInvestmentService; + + public UserController(ILogger logger, IUserInvestmentService userInvestmentService) + { + _logger = logger; + _userInvestmentService = userInvestmentService; + } + + //get endpoint exposing a given users investments + [HttpGet("{userId}/investments")] + public async Task GetUserInvestments(int userId) + { + _logger.LogInformation("GET: /" + userId + "/investments"); + var investments = await _userInvestmentService.GetUserInvestments(userId); + + if (investments == null) + { + _logger.LogInformation($"User with ID {userId} not found."); + return NotFound($"User with ID {userId} not found."); + } + + return Ok(investments); + } + + //get endpoint exposing a users certain investment details + [HttpGet("{userId}/investment/{investmentId}")] + public async Task GetUserInvestments(int userId, int investmentId) + { + _logger.LogInformation("GET: /" + userId + "/investment/" + investmentId); + var investment = await _userInvestmentService.GetUserInvestmentDetails(userId, investmentId); + + if (investment == null) + { + _logger.LogInformation($"Investment with ID {investmentId} not found."); + return NotFound($"Investment with ID {investmentId} not found."); + } + + return Ok(investment); + } + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/DTOs/InvestmentDTO.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/DTOs/InvestmentDTO.cs new file mode 100644 index 00000000..579d6bae --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/DTOs/InvestmentDTO.cs @@ -0,0 +1,18 @@ +using InvestmentPerformanceAPI.Enums; +using InvestmentPerformanceAPI.Models; + +namespace InvestmentPerformanceAPI.DTOs +{ + public class InvestmentDTO + { + public int Id { get; set; } + public required string Name { get; set; } + public decimal CostBasisPerShare { get; set; } + public decimal CurrentValue { get; set; } + public decimal CurrentPrice { get; set; } + public string? Term { get; set; } + public decimal TotalGains { get; set; } + public decimal NumberOfShares { get; set; } + + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/DTOs/UserInvestmentDTO.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/DTOs/UserInvestmentDTO.cs new file mode 100644 index 00000000..fb9c1fe6 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/DTOs/UserInvestmentDTO.cs @@ -0,0 +1,11 @@ +using InvestmentPerformanceAPI.Models; + +namespace InvestmentPerformanceAPI.DTOs +{ + public class UserInvestmentDTO + { + public required int InvestmentId { get; set; } = default; + public required string InvestmentName { get; set; } + + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Enums/TermEnum.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Enums/TermEnum.cs new file mode 100644 index 00000000..ddcabe9c --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Enums/TermEnum.cs @@ -0,0 +1,8 @@ +namespace InvestmentPerformanceAPI.Enums +{ + public enum TermEnum + { + Short = 0, + Long = 1, + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/IAppDbContext.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/IAppDbContext.cs new file mode 100644 index 00000000..b87a69e4 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/IAppDbContext.cs @@ -0,0 +1,13 @@ +using InvestmentPerformanceAPI.Models; +using Microsoft.EntityFrameworkCore; + +namespace InvestmentPerformanceAPI +{ + public interface IAppDbContext + { + DbSet Users { get; set; } + DbSet Investments { get; set; } + + Task SaveChangesAsync(CancellationToken cancellationToken = default); + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.csproj b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.csproj new file mode 100644 index 00000000..85e0331e --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.csproj @@ -0,0 +1,25 @@ + + + + net9.0 + enable + enable + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111170152_CreateUserAndInvestment.Designer.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111170152_CreateUserAndInvestment.Designer.cs new file mode 100644 index 00000000..5e067685 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111170152_CreateUserAndInvestment.Designer.cs @@ -0,0 +1,78 @@ +// +using InvestmentPerformanceAPI; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace InvestmentPerformanceAPI.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20251111170152_CreateUserAndInvestment")] + partial class CreateUserAndInvestment + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("InvestmentPerformanceAPI.Models.Investment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CostBasisPerShare") + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentValue") + .HasColumnType("decimal(18,2)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Term") + .HasColumnType("int"); + + b.Property("TotalGains") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.ToTable("Investments"); + }); + + modelBuilder.Entity("InvestmentPerformanceAPI.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111170152_CreateUserAndInvestment.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111170152_CreateUserAndInvestment.cs new file mode 100644 index 00000000..13ee9b53 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111170152_CreateUserAndInvestment.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace InvestmentPerformanceAPI.Migrations +{ + /// + public partial class CreateUserAndInvestment : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Investments", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false), + CostBasisPerShare = table.Column(type: "decimal(18,2)", nullable: false), + CurrentValue = table.Column(type: "decimal(18,2)", nullable: false), + CurrentPrice = table.Column(type: "decimal(18,2)", nullable: false), + Term = table.Column(type: "int", nullable: false), + TotalGains = table.Column(type: "decimal(18,2)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Investments", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Investments"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111185311_CreateInvesmentListOnUser.Designer.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111185311_CreateInvesmentListOnUser.Designer.cs new file mode 100644 index 00000000..1cc5ef5d --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111185311_CreateInvesmentListOnUser.Designer.cs @@ -0,0 +1,95 @@ +// +using InvestmentPerformanceAPI; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace InvestmentPerformanceAPI.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20251111185311_CreateInvesmentListOnUser")] + partial class CreateInvesmentListOnUser + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("InvestmentPerformanceAPI.Models.Investment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CostBasisPerShare") + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentValue") + .HasColumnType("decimal(18,2)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Term") + .HasColumnType("int"); + + b.Property("TotalGains") + .HasColumnType("decimal(18,2)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Investments"); + }); + + modelBuilder.Entity("InvestmentPerformanceAPI.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("InvestmentPerformanceAPI.Models.Investment", b => + { + b.HasOne("InvestmentPerformanceAPI.Models.User", null) + .WithMany("Investments") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("InvestmentPerformanceAPI.Models.User", b => + { + b.Navigation("Investments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111185311_CreateInvesmentListOnUser.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111185311_CreateInvesmentListOnUser.cs new file mode 100644 index 00000000..2a777d01 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111185311_CreateInvesmentListOnUser.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace InvestmentPerformanceAPI.Migrations +{ + /// + public partial class CreateInvesmentListOnUser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UserId", + table: "Investments", + type: "int", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Investments_UserId", + table: "Investments", + column: "UserId"); + + migrationBuilder.AddForeignKey( + name: "FK_Investments_Users_UserId", + table: "Investments", + column: "UserId", + principalTable: "Users", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Investments_Users_UserId", + table: "Investments"); + + migrationBuilder.DropIndex( + name: "IX_Investments_UserId", + table: "Investments"); + + migrationBuilder.DropColumn( + name: "UserId", + table: "Investments"); + } + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111195739_AddNumberOfShares.Designer.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111195739_AddNumberOfShares.Designer.cs new file mode 100644 index 00000000..9649ef8f --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111195739_AddNumberOfShares.Designer.cs @@ -0,0 +1,98 @@ +// +using InvestmentPerformanceAPI; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace InvestmentPerformanceAPI.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20251111195739_AddNumberOfShares")] + partial class AddNumberOfShares + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("InvestmentPerformanceAPI.Models.Investment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CostBasisPerShare") + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentValue") + .HasColumnType("decimal(18,2)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumberOfShares") + .HasColumnType("decimal(18,2)"); + + b.Property("Term") + .HasColumnType("int"); + + b.Property("TotalGains") + .HasColumnType("decimal(18,2)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Investments"); + }); + + modelBuilder.Entity("InvestmentPerformanceAPI.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("InvestmentPerformanceAPI.Models.Investment", b => + { + b.HasOne("InvestmentPerformanceAPI.Models.User", null) + .WithMany("Investments") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("InvestmentPerformanceAPI.Models.User", b => + { + b.Navigation("Investments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111195739_AddNumberOfShares.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111195739_AddNumberOfShares.cs new file mode 100644 index 00000000..5cb9561d --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111195739_AddNumberOfShares.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace InvestmentPerformanceAPI.Migrations +{ + /// + public partial class AddNumberOfShares : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "NumberOfShares", + table: "Investments", + type: "decimal(18,2)", + nullable: false, + defaultValue: 0m); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "NumberOfShares", + table: "Investments"); + } + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/AppDbContextModelSnapshot.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 00000000..fd636ae1 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,95 @@ +// +using InvestmentPerformanceAPI; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace InvestmentPerformanceAPI.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("InvestmentPerformanceAPI.Models.Investment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CostBasisPerShare") + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentValue") + .HasColumnType("decimal(18,2)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumberOfShares") + .HasColumnType("decimal(18,2)"); + + b.Property("Term") + .HasColumnType("int"); + + b.Property("TotalGains") + .HasColumnType("decimal(18,2)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Investments"); + }); + + modelBuilder.Entity("InvestmentPerformanceAPI.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("InvestmentPerformanceAPI.Models.Investment", b => + { + b.HasOne("InvestmentPerformanceAPI.Models.User", null) + .WithMany("Investments") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("InvestmentPerformanceAPI.Models.User", b => + { + b.Navigation("Investments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Models/Investment.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Models/Investment.cs new file mode 100644 index 00000000..98b9d25b --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Models/Investment.cs @@ -0,0 +1,16 @@ +using InvestmentPerformanceAPI.Enums; + +namespace InvestmentPerformanceAPI.Models +{ + public class Investment + { + public int Id { get; set; } + public required string Name { get; set; } + public decimal CostBasisPerShare { get; set; } + public decimal CurrentValue { get; set; } + public decimal CurrentPrice { get; set; } + public TermEnum Term { get; set; } + public decimal TotalGains { get; set; } + public decimal NumberOfShares { get; set; } + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Models/User.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Models/User.cs new file mode 100644 index 00000000..7a66bcda --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Models/User.cs @@ -0,0 +1,11 @@ +using InvestmentPerformanceAPI.Enums; + +namespace InvestmentPerformanceAPI.Models +{ + public class User + { + public int Id { get; set; } + public required string Name { get; set; } + public List Investments { get; set; } = new List(); + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Program.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Program.cs new file mode 100644 index 00000000..50ae72b5 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Program.cs @@ -0,0 +1,36 @@ +using InvestmentPerformanceAPI; +using InvestmentPerformanceAPI.Services; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); + +builder.Services.AddScoped(provider => provider.GetRequiredService()); +builder.Services.AddScoped(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); + + +public partial class Program { } \ No newline at end of file diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Properties/launchSettings.json b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Properties/launchSettings.json new file mode 100644 index 00000000..b148cc67 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5146", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7017;http://localhost:5146", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/IUserInvestmentService.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/IUserInvestmentService.cs new file mode 100644 index 00000000..fef351e6 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/IUserInvestmentService.cs @@ -0,0 +1,10 @@ +using InvestmentPerformanceAPI.DTOs; + +namespace InvestmentPerformanceAPI.Services +{ + public interface IUserInvestmentService + { + Task?> GetUserInvestments(int userId); + Task GetUserInvestmentDetails(int userId, int investmentId); + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/UserInvestmentService.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/UserInvestmentService.cs new file mode 100644 index 00000000..e43ae679 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/UserInvestmentService.cs @@ -0,0 +1,72 @@ +using InvestmentPerformanceAPI.DTOs; +using Microsoft.EntityFrameworkCore; + +namespace InvestmentPerformanceAPI.Services +{ + public class UserInvestmentService : IUserInvestmentService + { + private readonly ILogger _logger; + private readonly IAppDbContext _context; + + public UserInvestmentService(ILogger logger, IAppDbContext context) { + _logger = logger; + _context = context; + } + + //gets user investments via userId + public async Task?> GetUserInvestments(int userId) + { + _logger.LogInformation("UserInvestmentService - GetUserInvestments"); + var investments = await _context.Users + .Where(user => user.Id == userId) + .Select(user => user.Investments) + .FirstOrDefaultAsync(); + + _logger.LogInformation("UserInvestmentService - GetUserInvestments - Database Call Succesfull"); + + if (investments == null) + { + return null; + } + + return investments.Select(i => new UserInvestmentDTO + { + InvestmentId = i.Id, + InvestmentName = i.Name + + }).ToList(); + + } + //gets User Investment Details via a UserId and InvestmentId + public async Task GetUserInvestmentDetails(int userId, int investmentId) + { + _logger.LogInformation("UserInvestmentService - GetUserInvestmentDetails"); + var investment = await _context.Users + .Where(user => user.Id == userId) + .Select(user => user.Investments.Where(i => i.Id == investmentId).FirstOrDefault()) + .FirstOrDefaultAsync(); + + _logger.LogInformation("UserInvestmentService - GetUserInvestmentDetails - Database Call Succesfull"); + if (investment == null) + { + return null; + } + else + { + + return new InvestmentDTO + { + Id = investment.Id, + Name = investment.Name, + CostBasisPerShare = investment.CostBasisPerShare, + CurrentPrice = investment.CurrentPrice, + CurrentValue = investment.CurrentValue, + Term = investment.Term.ToString(), + TotalGains = investment.TotalGains, + NumberOfShares = investment.NumberOfShares, + }; + } + + } + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Controllers/UserControllerTests.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Controllers/UserControllerTests.cs new file mode 100644 index 00000000..0e9c21dc --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Controllers/UserControllerTests.cs @@ -0,0 +1,87 @@ +using InvestmentPerformanceAPI.Controllers; +using InvestmentPerformanceAPI.DTOs; +using InvestmentPerformanceAPI.Services; +using InvestmentPerformanceAPI.Tests.Services; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using Moq; +using NUnit.Framework; + +namespace InvestmentPerformanceAPI.Tests.Controllers +{ + public class UsersControllerTests + { + public required Mock _serviceMock; + public required Mock> _loggerMock; + public required UserController _controller; + + [OneTimeSetUp] + public void Setup() + { + _serviceMock = new Mock(); + _loggerMock = new Mock>(); + _controller = new UserController(_loggerMock.Object, _serviceMock.Object); + } + + [Test] + public async Task GetUserInvestments_ReturnsOk_WhenInvestmentsExist() + { + var investments = new List + { + new UserInvestmentDTO { InvestmentId = 1, InvestmentName = "QQQ" } + }; + _serviceMock.Setup(s => s.GetUserInvestments(1)).ReturnsAsync(investments); + + var result = await _controller.GetUserInvestments(1); + + Assert.That(result, Is.InstanceOf()); + + var okResult = result as OkObjectResult; + Assert.That(investments, Is.EqualTo(okResult?.Value)); + } + + [Test] + public async Task GetUserInvestments_ReturnsNotFound_WhenUserDoesNotExist() + { + _serviceMock.Setup(s => s.GetUserInvestments(999)).ReturnsAsync((List?)null); + + var result = await _controller.GetUserInvestments(999); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public async Task GetUserInvestmentDetails_ReturnsNotFound_WhenUserDoesNotExist() + { + _serviceMock.Setup(s => s.GetUserInvestmentDetails(99, 3)).ReturnsAsync((InvestmentDTO?)null); + + var result = await _controller.GetUserInvestments(999); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public async Task GetUserInvestmentDetails_ReturnsOk_WhenUserInvestmentsExist() + { + var investment = new InvestmentDTO + { + Id = 4, + CostBasisPerShare = 12, + CurrentPrice = 15, + CurrentValue = 12, + NumberOfShares = 4, + Name = "Test", + Term = "Long", + TotalGains = 4000, + }; + _serviceMock.Setup(s => s.GetUserInvestmentDetails(1, 4)).ReturnsAsync(investment); + + var result = await _controller.GetUserInvestments(1, 4); + + Assert.That(result, Is.InstanceOf()); + + var okResult = result as OkObjectResult; + Assert.That(investment, Is.EqualTo(okResult.Value)); + } + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Services/UserInvestmentServiceTests.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Services/UserInvestmentServiceTests.cs new file mode 100644 index 00000000..cbad6e9a --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Services/UserInvestmentServiceTests.cs @@ -0,0 +1,94 @@ +using InvestmentPerformanceAPI.Enums; +using InvestmentPerformanceAPI.Models; +using InvestmentPerformanceAPI.Services; +using Microsoft.EntityFrameworkCore; +using Moq; +using NUnit.Framework; + +namespace InvestmentPerformanceAPI.Tests.Services +{ + public class UserInvestmentServiceTests + { + public required AppDbContext _context; + public required UserInvestmentService _service; + + [OneTimeSetUp] + public void Setup() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "TestDb") + .Options; + + _context = new AppDbContext(options); + + var mockLogger = new Mock>(); + + // Seed data + var user = new User + { + Id = 1, + Name = "Rudy", + Investments = new List + { + new Investment { Id = 1, Name = "QQQ", CostBasisPerShare = 40, CurrentPrice = 70, NumberOfShares = 1, CurrentValue = 70, Term = TermEnum.Short, TotalGains = 30}, + new Investment { Id = 2, Name = "SPY", CostBasisPerShare = 50, CurrentPrice = 80, NumberOfShares = 2, CurrentValue = 160, Term = TermEnum.Long, TotalGains = 60}, + new Investment { Id = 3, Name = "SPX", CostBasisPerShare = 60, CurrentPrice = 90, NumberOfShares = 3, CurrentValue = 270, Term = TermEnum.Long, TotalGains = 90}, + } + }; + _context.Users.Add(user); + _context.SaveChanges(); + + _service = new UserInvestmentService(mockLogger.Object, _context); + } + + [Test] + public async Task GetUserInvestments_ReturnsInvestments_WhenUserExists() + { + var result = await _service.GetUserInvestments(1); + + Assert.That(result, Is.Not.Null); + Assert.That(result?.Count, Is.EqualTo(3)); + Assert.That(result?[0].InvestmentName, Is.EqualTo("QQQ")); + Assert.That(result?[1].InvestmentName, Is.EqualTo("SPY")); + Assert.That(result?[1].InvestmentId, Is.EqualTo(2)); + } + + [Test] + public async Task GetUserInvestments_ReturnsNull_WhenUserDoesNotExist() + { + var result = await _service.GetUserInvestments(4); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task GetUserInvestmentDetails_ReturnsInvestmentsDetails_WhenUserAndInvestmentExists() + { + var result = await _service.GetUserInvestmentDetails(1,3); + + Assert.That(result, Is.Not.Null); + Assert.That(result?.Id, Is.EqualTo(3)); + Assert.That(result?.CurrentValue, Is.EqualTo(270)); + Assert.That(result?.CostBasisPerShare, Is.EqualTo(60)); + Assert.That(result?.CurrentPrice, Is.EqualTo(90)); + Assert.That(result?.Term, Is.EqualTo(TermEnum.Long.ToString())); + Assert.That(result?.TotalGains, Is.EqualTo(90)); + } + + [Test] + public async Task GetUserInvestmentDetails_ReturnsNull_WhenUserDoesntExist() + { + var result = await _service.GetUserInvestmentDetails(5, 3); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task GetUserInvestmentDetails_ReturnsNull_WhenInvestmentDoesntExist() + { + var result = await _service.GetUserInvestmentDetails(1, 5); + + Assert.That(result, Is.Null); + } + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/appsettings.Development.json b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/appsettings.json b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/appsettings.json new file mode 100644 index 00000000..c37f4b4b --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/appsettings.json @@ -0,0 +1,12 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=InvestmentPerformance;Trusted_Connection=True;" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/frontendMockup.png b/frontendMockup.png new file mode 100644 index 00000000..0dc901cb Binary files /dev/null and b/frontendMockup.png differ diff --git a/instructions.md b/instructions.md new file mode 100644 index 00000000..bdc17a3c --- /dev/null +++ b/instructions.md @@ -0,0 +1,77 @@ +General Information + + - It is a restAPI written in C# .NET 9 + - The database I used was SqlServer + - Routes + - Get a list of current investments for the user + - http://localhost:5146/user/{userId}/investments + - The return object looks like the following + - [ + { + "investmentId": 2, + "investmentName": "SPY" + }, + { + "investmentId": 3, + "investmentName": "SPX" + } + ] + + - Get details for a user's investment + - http://localhost:5146/user/{userId}/investment/{investmentId} + - The return object looks like the following + - { + "id": 2, + "name": "SPY", + "costBasisPerShare": 4.00, + "currentValue": 5.00, + "currentPrice": 5.00, + "term": "Short", + "totalGains": 12.00, + "numberOfShares": 2.00 + } + + +Setup + + - The project needs the following packages to be installed + - EntityFrameworkCore 9.0.11 + - EntityFrameworkCore.InMemory 9.0.11 + - EntityFrameworkCore.SqlServer 9.0.11 + - EntityFrameworkCore.Tools 9.0.11 + - Microsoft.NET.Test.Sdk 18.0.1 + - Moq 4.20.72 + - NUnit 4.4.0 + - NUnitTestAdapter 5.2.0 + + - The integration test project needs the following to be installed + - coverlet.collector 6.0.2 + - Microsoft.AspNetCore.Mvc.Testing 9.0.11 + - EntityFrameworkCore 9.0.11 + - EntityFrameworkCore.InMemory 9.0.11 + - Microsoft.NET.Test.Sdk 18.0.1 + - NUnit 4.4.0 + - NUnitTestAdapter 5.2.0 + + + I used .NET migrations to handle the database to get your database ready + - Update the connectionString in appsettings.json + - run Update-Database in the Package Manager Console + + At this point if you are running in Visual Studio you can press Play to start the API + or navigate to InvestmentPerformanceAPI and run dotnet run + + +Testing + + - There are integration tests and unit tests + - Unit Tests are located in the InvestmentPerfomanceAPI and Integration Tests are located in InvestmentPerfomanceAPI.IntegrationTests + + -Integration Tests use an inMemoryDatabase + + - All tests should be passing and can be run via the test explorer in Visual Studio + +Misc + + - I added a little frontend mockup as a png in the project + - Since a frontend component was not a part of this exercise, I wanted to illustrate how you might use both of these endpoints together \ No newline at end of file diff --git a/readme.txt b/readme.txt new file mode 100644 index 00000000..bdc17a3c --- /dev/null +++ b/readme.txt @@ -0,0 +1,77 @@ +General Information + + - It is a restAPI written in C# .NET 9 + - The database I used was SqlServer + - Routes + - Get a list of current investments for the user + - http://localhost:5146/user/{userId}/investments + - The return object looks like the following + - [ + { + "investmentId": 2, + "investmentName": "SPY" + }, + { + "investmentId": 3, + "investmentName": "SPX" + } + ] + + - Get details for a user's investment + - http://localhost:5146/user/{userId}/investment/{investmentId} + - The return object looks like the following + - { + "id": 2, + "name": "SPY", + "costBasisPerShare": 4.00, + "currentValue": 5.00, + "currentPrice": 5.00, + "term": "Short", + "totalGains": 12.00, + "numberOfShares": 2.00 + } + + +Setup + + - The project needs the following packages to be installed + - EntityFrameworkCore 9.0.11 + - EntityFrameworkCore.InMemory 9.0.11 + - EntityFrameworkCore.SqlServer 9.0.11 + - EntityFrameworkCore.Tools 9.0.11 + - Microsoft.NET.Test.Sdk 18.0.1 + - Moq 4.20.72 + - NUnit 4.4.0 + - NUnitTestAdapter 5.2.0 + + - The integration test project needs the following to be installed + - coverlet.collector 6.0.2 + - Microsoft.AspNetCore.Mvc.Testing 9.0.11 + - EntityFrameworkCore 9.0.11 + - EntityFrameworkCore.InMemory 9.0.11 + - Microsoft.NET.Test.Sdk 18.0.1 + - NUnit 4.4.0 + - NUnitTestAdapter 5.2.0 + + + I used .NET migrations to handle the database to get your database ready + - Update the connectionString in appsettings.json + - run Update-Database in the Package Manager Console + + At this point if you are running in Visual Studio you can press Play to start the API + or navigate to InvestmentPerformanceAPI and run dotnet run + + +Testing + + - There are integration tests and unit tests + - Unit Tests are located in the InvestmentPerfomanceAPI and Integration Tests are located in InvestmentPerfomanceAPI.IntegrationTests + + -Integration Tests use an inMemoryDatabase + + - All tests should be passing and can be run via the test explorer in Visual Studio + +Misc + + - I added a little frontend mockup as a png in the project + - Since a frontend component was not a part of this exercise, I wanted to illustrate how you might use both of these endpoints together \ No newline at end of file