From bf2fbf65a1adc2b265dcce1bbe507fe7807e7676 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 11 Nov 2025 14:30:15 -0500 Subject: [PATCH 1/7] controllers up and running --- .gitignore | 93 ++++++++++++++++++ .../InvestmentPerformanceAPI.sln | 25 +++++ .../InvestmentPerformanceAPI/AppDbContext.cs | 18 ++++ .../Controllers/UserController.cs | 51 ++++++++++ .../DTOs/InvestmentDTO.cs | 17 ++++ .../DTOs/UserInvestmentDTO.cs | 11 +++ .../Enums/TermEnum.cs | 8 ++ .../InvestmentPerformanceAPI/IAppDbContext.cs | 13 +++ .../InvestmentPerformanceAPI.csproj | 19 ++++ .../InvestmentPerformanceAPI.http | 6 ++ ...170152_CreateUserAndInvestment.Designer.cs | 78 +++++++++++++++ .../20251111170152_CreateUserAndInvestment.cs | 55 +++++++++++ ...5311_CreateInvesmentListOnUser.Designer.cs | 95 +++++++++++++++++++ ...0251111185311_CreateInvesmentListOnUser.cs | 48 ++++++++++ .../Migrations/AppDbContextModelSnapshot.cs | 92 ++++++++++++++++++ .../Models/Investment.cs | 15 +++ .../InvestmentPerformanceAPI/Models/User.cs | 11 +++ .../InvestmentPerformanceAPI/Program.cs | 33 +++++++ .../Properties/launchSettings.json | 23 +++++ .../Services/IUserInvestmentService.cs | 10 ++ .../Services/UserInvestmentService.cs | 69 ++++++++++++++ .../appsettings.Development.json | 8 ++ .../InvestmentPerformanceAPI/appsettings.json | 12 +++ 23 files changed, 810 insertions(+) create mode 100644 .gitignore create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI.sln create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/AppDbContext.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/Controllers/UserController.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/DTOs/InvestmentDTO.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/DTOs/UserInvestmentDTO.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/Enums/TermEnum.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/IAppDbContext.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.csproj create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.http create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111170152_CreateUserAndInvestment.Designer.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111170152_CreateUserAndInvestment.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111185311_CreateInvesmentListOnUser.Designer.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111185311_CreateInvesmentListOnUser.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/AppDbContextModelSnapshot.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/Models/Investment.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/Models/User.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/Program.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/Properties/launchSettings.json create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/IUserInvestmentService.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/UserInvestmentService.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/appsettings.Development.json create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/appsettings.json 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.sln b/InvestmentPerformanceAPI/InvestmentPerformanceAPI.sln new file mode 100644 index 00000000..78ed9f19 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36705.20 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InvestmentPerformanceAPI", "InvestmentPerformanceAPI\InvestmentPerformanceAPI.csproj", "{58D4DC25-A7FF-499C-9985-CFF8F3A3E409}" +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 + 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..baf7f034 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Controllers/UserController.cs @@ -0,0 +1,51 @@ +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; + } + + [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); + } + + [HttpGet("{userId}/investments/{investmentId}")] + public async Task GetUserInvestments(int userId, int investmentId) + { + _logger.LogInformation("GET: /" + userId + "/investments/" + 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..bc1cefc1 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/DTOs/InvestmentDTO.cs @@ -0,0 +1,17 @@ +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; } + + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/DTOs/UserInvestmentDTO.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/DTOs/UserInvestmentDTO.cs new file mode 100644 index 00000000..14b8fcce --- /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; } = default; + + } +} 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..73fca661 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + enable + enable + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.http b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.http new file mode 100644 index 00000000..4338f2fb --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.http @@ -0,0 +1,6 @@ +@InvestmentPerformanceAPI_HostAddress = http://localhost:5146 + +GET {{InvestmentPerformanceAPI_HostAddress}}/weatherforecast/ +Accept: application/json + +### 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/AppDbContextModelSnapshot.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 00000000..0dd1b9e7 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,92 @@ +// +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("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..016e1edc --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Models/Investment.cs @@ -0,0 +1,15 @@ +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; } + } +} 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..2dd354c2 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Program.cs @@ -0,0 +1,33 @@ +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(); 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..d2e63de7 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/UserInvestmentService.cs @@ -0,0 +1,69 @@ +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; + } + + 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 new List(); + } + + return investments.Select(i => new UserInvestmentDTO + { + InvestmentId = i.Id, + InvestmentName = i.Name + + }).ToList(); + + } + 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).First()) + .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, + }; + } + + } + } +} 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 From 29ac489351b92cd2f664da052dc2a86d0fbd69ca Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 11 Nov 2025 15:22:48 -0500 Subject: [PATCH 2/7] adding unit tests for service --- .../DTOs/InvestmentDTO.cs | 1 + .../DTOs/UserInvestmentDTO.cs | 2 +- .../InvestmentPerformanceAPI.csproj | 5 + ...251111195739_AddNumberOfShares.Designer.cs | 98 +++++++++++++++++++ .../20251111195739_AddNumberOfShares.cs | 29 ++++++ .../Migrations/AppDbContextModelSnapshot.cs | 3 + .../Models/Investment.cs | 1 + .../Services/UserInvestmentService.cs | 3 +- .../Tests/Controllers/UserControllerTests.cs | 6 ++ .../Services/UserInvestmentServiceTests.cs | 95 ++++++++++++++++++ 10 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111195739_AddNumberOfShares.Designer.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/20251111195739_AddNumberOfShares.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Controllers/UserControllerTests.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Services/UserInvestmentServiceTests.cs diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/DTOs/InvestmentDTO.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/DTOs/InvestmentDTO.cs index bc1cefc1..579d6bae 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/DTOs/InvestmentDTO.cs +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/DTOs/InvestmentDTO.cs @@ -12,6 +12,7 @@ public class InvestmentDTO 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 index 14b8fcce..fb9c1fe6 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/DTOs/UserInvestmentDTO.cs +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/DTOs/UserInvestmentDTO.cs @@ -5,7 +5,7 @@ namespace InvestmentPerformanceAPI.DTOs public class UserInvestmentDTO { public required int InvestmentId { get; set; } = default; - public required string InvestmentName { get; set; } = default; + public required string InvestmentName { get; set; } } } diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.csproj b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.csproj index 73fca661..e630e1c0 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.csproj +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.csproj @@ -9,11 +9,16 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive all + + + + 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 index 0dd1b9e7..fd636ae1 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/AppDbContextModelSnapshot.cs +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Migrations/AppDbContextModelSnapshot.cs @@ -42,6 +42,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("NumberOfShares") + .HasColumnType("decimal(18,2)"); + b.Property("Term") .HasColumnType("int"); diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Models/Investment.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Models/Investment.cs index 016e1edc..98b9d25b 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Models/Investment.cs +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Models/Investment.cs @@ -11,5 +11,6 @@ public class Investment 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/Services/UserInvestmentService.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/UserInvestmentService.cs index d2e63de7..f5cce5ce 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/UserInvestmentService.cs +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/UserInvestmentService.cs @@ -41,7 +41,7 @@ public UserInvestmentService(ILogger logger, IAppDbContex _logger.LogInformation("UserInvestmentService - GetUserInvestmentDetails"); var investment = await _context.Users .Where(user => user.Id == userId) - .Select(user => user.Investments.Where(i => i.Id == investmentId).First()) + .Select(user => user.Investments.Where(i => i.Id == investmentId).FirstOrDefault()) .FirstOrDefaultAsync(); _logger.LogInformation("UserInvestmentService - GetUserInvestmentDetails - Database Call Succesfull"); @@ -61,6 +61,7 @@ public UserInvestmentService(ILogger logger, IAppDbContex 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..acbed2c2 --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Controllers/UserControllerTests.cs @@ -0,0 +1,6 @@ +namespace InvestmentPerformanceAPI.Tests.Controllers +{ + public class UserControllerTests + { + } +} diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Services/UserInvestmentServiceTests.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Services/UserInvestmentServiceTests.cs new file mode 100644 index 00000000..0b9239cc --- /dev/null +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Services/UserInvestmentServiceTests.cs @@ -0,0 +1,95 @@ +using InvestmentPerformanceAPI.Enums; +using InvestmentPerformanceAPI.Models; +using InvestmentPerformanceAPI.Services; +using Microsoft.EntityFrameworkCore; +using Moq; +using NUnit.Framework; + +namespace InvestmentPerformanceAPI.Tests.Services +{ + public class UserInvestmentServiceTests + { + private AppDbContext _context; + private UserInvestmentService _service; + private Logger _logger; + + [SetUp] + 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_ReturnsEmpty_WhenUserDoesNotExist() + { + var result = await _service.GetUserInvestments(4); + + Assert.That(result, Is.Empty); + } + + [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); + } + } +} From 2795fbfc60bbca9e963a2dc109df7cef4b065deb Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 11 Nov 2025 15:42:11 -0500 Subject: [PATCH 3/7] adding controller unit tests --- .../Tests/Controllers/UserControllerTests.cs | 85 ++++++++++++++++++- .../Services/UserInvestmentServiceTests.cs | 2 +- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Controllers/UserControllerTests.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Controllers/UserControllerTests.cs index acbed2c2..36dc9b64 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Controllers/UserControllerTests.cs +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Controllers/UserControllerTests.cs @@ -1,6 +1,87 @@ -namespace InvestmentPerformanceAPI.Tests.Controllers +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 UserControllerTests + public class UsersControllerTests { + private Mock _serviceMock; + private Mock> _loggerMock; + private 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 index 0b9239cc..075ac750 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Services/UserInvestmentServiceTests.cs +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Services/UserInvestmentServiceTests.cs @@ -13,7 +13,7 @@ public class UserInvestmentServiceTests private UserInvestmentService _service; private Logger _logger; - [SetUp] + [OneTimeSetUp] public void Setup() { var options = new DbContextOptionsBuilder() From 20465518273b97c78245190708b3d2c52afeb0e4 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 11 Nov 2025 16:51:15 -0500 Subject: [PATCH 4/7] adding integration tests --- .../IntegrationTests.cs | 98 +++++++++++++++++++ ...mentPerformanceAPI.IntegrationTests.csproj | 30 ++++++ .../InvestmentPerformanceAPI.sln | 8 +- .../InvestmentPerformanceAPI.csproj | 1 + .../InvestmentPerformanceAPI/Program.cs | 3 + .../Services/UserInvestmentService.cs | 2 +- .../Services/UserInvestmentServiceTests.cs | 4 +- 7 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI.IntegrationTests/IntegrationTests.cs create mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI.IntegrationTests/InvestmentPerformanceAPI.IntegrationTests.csproj diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI.IntegrationTests/IntegrationTests.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI.IntegrationTests/IntegrationTests.cs new file mode 100644 index 00000000..16572935 --- /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/investments/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/investments/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 index 78ed9f19..76201684 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI.sln +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI.sln @@ -1,10 +1,12 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.14.36705.20 d17.14 +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 @@ -15,6 +17,10 @@ Global {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 diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.csproj b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.csproj index e630e1c0..85e0331e 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.csproj +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.csproj @@ -4,6 +4,7 @@ net9.0 enable enable + true diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Program.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Program.cs index 2dd354c2..50ae72b5 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Program.cs +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Program.cs @@ -31,3 +31,6 @@ app.MapControllers(); app.Run(); + + +public partial class Program { } \ No newline at end of file diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/UserInvestmentService.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/UserInvestmentService.cs index f5cce5ce..ad13626b 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/UserInvestmentService.cs +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/UserInvestmentService.cs @@ -25,7 +25,7 @@ public UserInvestmentService(ILogger logger, IAppDbContex if (investments == null) { - return new List(); + return null; } return investments.Select(i => new UserInvestmentDTO diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Services/UserInvestmentServiceTests.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Services/UserInvestmentServiceTests.cs index 075ac750..9cf70fb4 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Services/UserInvestmentServiceTests.cs +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Services/UserInvestmentServiceTests.cs @@ -55,11 +55,11 @@ public async Task GetUserInvestments_ReturnsInvestments_WhenUserExists() } [Test] - public async Task GetUserInvestments_ReturnsEmpty_WhenUserDoesNotExist() + public async Task GetUserInvestments_ReturnsNull_WhenUserDoesNotExist() { var result = await _service.GetUserInvestments(4); - Assert.That(result, Is.Empty); + Assert.That(result, Is.Null); } [Test] From af13d3f877eedbde89106d17b9b7fb8c6fdc70fb Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 11 Nov 2025 17:31:44 -0500 Subject: [PATCH 5/7] documentation --- frontendMockup.png | Bin 0 -> 52647 bytes readme.txt | 74 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 frontendMockup.png create mode 100644 readme.txt diff --git a/frontendMockup.png b/frontendMockup.png new file mode 100644 index 0000000000000000000000000000000000000000..0dc901cb24700e0f17e88551350c425c5b135ea2 GIT binary patch literal 52647 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 Date: Tue, 11 Nov 2025 17:50:23 -0500 Subject: [PATCH 6/7] supressing some warnings and adding instructions and readme --- .../Controllers/UserController.cs | 2 + .../Services/UserInvestmentService.cs | 2 + .../Tests/Controllers/UserControllerTests.cs | 8 +- .../Services/UserInvestmentServiceTests.cs | 25 +++--- instructions.md | 77 +++++++++++++++++++ readme.txt | 3 + 6 files changed, 100 insertions(+), 17 deletions(-) create mode 100644 instructions.md diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Controllers/UserController.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Controllers/UserController.cs index baf7f034..29a6f68f 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Controllers/UserController.cs +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Controllers/UserController.cs @@ -18,6 +18,7 @@ public UserController(ILogger logger, IUserInvestmentService use _userInvestmentService = userInvestmentService; } + //get endpoint exposing a given users investments [HttpGet("{userId}/investments")] public async Task GetUserInvestments(int userId) { @@ -33,6 +34,7 @@ public async Task GetUserInvestments(int userId) return Ok(investments); } + //get endpoint exposing a users certain investment details [HttpGet("{userId}/investments/{investmentId}")] public async Task GetUserInvestments(int userId, int investmentId) { diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/UserInvestmentService.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/UserInvestmentService.cs index ad13626b..e43ae679 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/UserInvestmentService.cs +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Services/UserInvestmentService.cs @@ -13,6 +13,7 @@ public UserInvestmentService(ILogger logger, IAppDbContex _context = context; } + //gets user investments via userId public async Task?> GetUserInvestments(int userId) { _logger.LogInformation("UserInvestmentService - GetUserInvestments"); @@ -36,6 +37,7 @@ public UserInvestmentService(ILogger logger, IAppDbContex }).ToList(); } + //gets User Investment Details via a UserId and InvestmentId public async Task GetUserInvestmentDetails(int userId, int investmentId) { _logger.LogInformation("UserInvestmentService - GetUserInvestmentDetails"); diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Controllers/UserControllerTests.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Controllers/UserControllerTests.cs index 36dc9b64..0e9c21dc 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Controllers/UserControllerTests.cs +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Controllers/UserControllerTests.cs @@ -11,9 +11,9 @@ namespace InvestmentPerformanceAPI.Tests.Controllers { public class UsersControllerTests { - private Mock _serviceMock; - private Mock> _loggerMock; - private UserController _controller; + public required Mock _serviceMock; + public required Mock> _loggerMock; + public required UserController _controller; [OneTimeSetUp] public void Setup() @@ -37,7 +37,7 @@ public async Task GetUserInvestments_ReturnsOk_WhenInvestmentsExist() Assert.That(result, Is.InstanceOf()); var okResult = result as OkObjectResult; - Assert.That(investments, Is.EqualTo(okResult.Value)); + Assert.That(investments, Is.EqualTo(okResult?.Value)); } [Test] diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Services/UserInvestmentServiceTests.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Services/UserInvestmentServiceTests.cs index 9cf70fb4..cbad6e9a 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Services/UserInvestmentServiceTests.cs +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Tests/Services/UserInvestmentServiceTests.cs @@ -9,9 +9,8 @@ namespace InvestmentPerformanceAPI.Tests.Services { public class UserInvestmentServiceTests { - private AppDbContext _context; - private UserInvestmentService _service; - private Logger _logger; + public required AppDbContext _context; + public required UserInvestmentService _service; [OneTimeSetUp] public void Setup() @@ -48,10 +47,10 @@ 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)); + 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] @@ -68,12 +67,12 @@ public async Task GetUserInvestmentDetails_ReturnsInvestmentsDetails_WhenUserAnd 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)); + 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] 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 index 0fd8f476..bdc17a3c 100644 --- a/readme.txt +++ b/readme.txt @@ -33,6 +33,7 @@ General Information Setup + - The project needs the following packages to be installed - EntityFrameworkCore 9.0.11 - EntityFrameworkCore.InMemory 9.0.11 @@ -62,6 +63,7 @@ Setup Testing + - There are integration tests and unit tests - Unit Tests are located in the InvestmentPerfomanceAPI and Integration Tests are located in InvestmentPerfomanceAPI.IntegrationTests @@ -70,5 +72,6 @@ Testing - 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 From 465b2a64ee293aafe9c3964ecd0b8a6e4b7cb9b8 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 11 Nov 2025 17:57:47 -0500 Subject: [PATCH 7/7] cleaning up verbs --- .../IntegrationTests.cs | 4 ++-- .../InvestmentPerformanceAPI/Controllers/UserController.cs | 4 ++-- .../InvestmentPerformanceAPI/InvestmentPerformanceAPI.http | 6 ------ 3 files changed, 4 insertions(+), 10 deletions(-) delete mode 100644 InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.http diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI.IntegrationTests/IntegrationTests.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI.IntegrationTests/IntegrationTests.cs index 16572935..a27201fa 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI.IntegrationTests/IntegrationTests.cs +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI.IntegrationTests/IntegrationTests.cs @@ -81,7 +81,7 @@ public async Task GetUserInvestments_ReturnsNotFound_WhenUserDoesNotExist() [Test] public async Task GetUserInvestmentDetails_ReturnsOk_WhenUserExists() { - var response = await _client.GetAsync("/user/1/investments/2"); + var response = await _client.GetAsync("/user/1/investment/2"); response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsStringAsync(); @@ -91,7 +91,7 @@ public async Task GetUserInvestmentDetails_ReturnsOk_WhenUserExists() [Test] public async Task GetUserInvestmentDetails_ReturnsNotFound_WhenUserDoesNotExist() { - var response = await _client.GetAsync("user/99/investments/2"); + var response = await _client.GetAsync("user/99/investment/2"); Assert.That(response.StatusCode, Is.EqualTo(System.Net.HttpStatusCode.NotFound)); } } diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Controllers/UserController.cs b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Controllers/UserController.cs index 29a6f68f..f9579b80 100644 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Controllers/UserController.cs +++ b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/Controllers/UserController.cs @@ -35,10 +35,10 @@ public async Task GetUserInvestments(int userId) } //get endpoint exposing a users certain investment details - [HttpGet("{userId}/investments/{investmentId}")] + [HttpGet("{userId}/investment/{investmentId}")] public async Task GetUserInvestments(int userId, int investmentId) { - _logger.LogInformation("GET: /" + userId + "/investments/" + investmentId); + _logger.LogInformation("GET: /" + userId + "/investment/" + investmentId); var investment = await _userInvestmentService.GetUserInvestmentDetails(userId, investmentId); if (investment == null) diff --git a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.http b/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.http deleted file mode 100644 index 4338f2fb..00000000 --- a/InvestmentPerformanceAPI/InvestmentPerformanceAPI/InvestmentPerformanceAPI.http +++ /dev/null @@ -1,6 +0,0 @@ -@InvestmentPerformanceAPI_HostAddress = http://localhost:5146 - -GET {{InvestmentPerformanceAPI_HostAddress}}/weatherforecast/ -Accept: application/json - -###