Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions InvestmentWebAPI/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
InvestmentPerformanceWebAPI/bin/
InvestmentPerformanceWebAPI/obj/
InvestmentPerformanceWebAPI/Migrations/obj/
InvestmentPerformanceWebAPI/Migrations/bin/
InvestmentPerformanceWebAPI.Tests/bin/
InvestmentPerformanceWebAPI.Tests/obj/

InvestmentPerformanceWebAPI/*.db*
*.db
*.db-shm
*.db-wal
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using InvestmentPerformanceWebAPI.Controllers;
using InvestmentPerformanceWebAPI.Data;
using InvestmentPerformanceWebAPI.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;

namespace InvestmentPerformanceWebAPI.Tests.Controllers
{
public class InvestmentControllerTests
{
private InvestmentDbContext GetInMemoryDbContext()
{
var options = new DbContextOptionsBuilder<InvestmentDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;

var context = new InvestmentDbContext(options);

var user = new User
{
UserId = 1,
Name = "Alice",
Investments = new List<Investment>
{
new Investment
{
InvestmentId = 1,
UserId = 1,
Name = "Apple Inc.",
Ticker = "AAPL",
NumberOfShares = 10,
CostBasisPerShare = 120,
CurrentPrice = 175,
TimeOfPurchase = new DateTime(2023, 6, 10)
}
}
};

context.Users.Add(user);
context.SaveChanges();

return context;
}

[Fact]
public async Task GetInvestmentsForUser_ReturnsInvestments_WhenUserExists()
{
var context = GetInMemoryDbContext();
var logger = Mock.Of<ILogger<InvestmentController>>();
var controller = new InvestmentController(context, logger);

var result = await controller.GetInvestmentsForUser(1);

var okResult = Assert.IsType<OkObjectResult>(result.Result);
var investments = Assert.IsAssignableFrom<IEnumerable<object>>(okResult.Value);
Assert.Single(investments);
}

[Fact]
public async Task GetInvestmentDetails_ReturnsCorrectDetails_WhenInvestmentExists()
{
var context = GetInMemoryDbContext();
var logger = Mock.Of<ILogger<InvestmentController>>();
var controller = new InvestmentController(context, logger);

var result = await controller.GetInvestmentDetails(1, 1);

var okResult = Assert.IsType<OkObjectResult>(result.Result);
var investment = okResult.Value;
Assert.NotNull(investment);

var name = investment.GetType().GetProperty("Name")?.GetValue(investment);
Assert.Equal("Apple Inc.", name);
}

[Fact]
public async Task GetInvestmentsForUser_ReturnsNotFound_WhenUserDoesNotExist()
{
var context = GetInMemoryDbContext();
var logger = Mock.Of<ILogger<InvestmentController>>();
var controller = new InvestmentController(context, logger);

var result = await controller.GetInvestmentsForUser(13);

var notFoundResult = Assert.IsType<NotFoundObjectResult>(result.Result);
Assert.Equal("User 13 not found", notFoundResult.Value);
}

[Fact]
public async Task GetInvestmentDetails_ReturnsNotFound_WhenInvestmentDoesNotExist()
{
var context = GetInMemoryDbContext();
var logger = Mock.Of<ILogger<InvestmentController>>();
var controller = new InvestmentController(context, logger);

var result = await controller.GetInvestmentDetails(1, 50);

var notFoundResult = Assert.IsType<NotFoundObjectResult>(result.Result);
Assert.Equal("Investment 50 not found for user 1", notFoundResult.Value);
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\InvestmentPerformanceWebAPI\InvestmentPerformanceWebAPI.csproj" />
</ItemGroup>

</Project>
48 changes: 48 additions & 0 deletions InvestmentWebAPI/InvestmentPerformanceWebAPI.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InvestmentPerformanceWebAPI", "InvestmentPerformanceWebAPI\InvestmentPerformanceWebAPI.csproj", "{CC862C88-821D-4AEF-A507-B3A49D42F158}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InvestmentPerformanceWebAPI.Tests", "InvestmentPerformanceWebAPI.Tests\InvestmentPerformanceWebAPI.Tests.csproj", "{AABDE67A-1EB9-4B52-B8E3-48D483E1F026}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CC862C88-821D-4AEF-A507-B3A49D42F158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CC862C88-821D-4AEF-A507-B3A49D42F158}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC862C88-821D-4AEF-A507-B3A49D42F158}.Debug|x64.ActiveCfg = Debug|Any CPU
{CC862C88-821D-4AEF-A507-B3A49D42F158}.Debug|x64.Build.0 = Debug|Any CPU
{CC862C88-821D-4AEF-A507-B3A49D42F158}.Debug|x86.ActiveCfg = Debug|Any CPU
{CC862C88-821D-4AEF-A507-B3A49D42F158}.Debug|x86.Build.0 = Debug|Any CPU
{CC862C88-821D-4AEF-A507-B3A49D42F158}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC862C88-821D-4AEF-A507-B3A49D42F158}.Release|Any CPU.Build.0 = Release|Any CPU
{CC862C88-821D-4AEF-A507-B3A49D42F158}.Release|x64.ActiveCfg = Release|Any CPU
{CC862C88-821D-4AEF-A507-B3A49D42F158}.Release|x64.Build.0 = Release|Any CPU
{CC862C88-821D-4AEF-A507-B3A49D42F158}.Release|x86.ActiveCfg = Release|Any CPU
{CC862C88-821D-4AEF-A507-B3A49D42F158}.Release|x86.Build.0 = Release|Any CPU
{AABDE67A-1EB9-4B52-B8E3-48D483E1F026}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AABDE67A-1EB9-4B52-B8E3-48D483E1F026}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AABDE67A-1EB9-4B52-B8E3-48D483E1F026}.Debug|x64.ActiveCfg = Debug|Any CPU
{AABDE67A-1EB9-4B52-B8E3-48D483E1F026}.Debug|x64.Build.0 = Debug|Any CPU
{AABDE67A-1EB9-4B52-B8E3-48D483E1F026}.Debug|x86.ActiveCfg = Debug|Any CPU
{AABDE67A-1EB9-4B52-B8E3-48D483E1F026}.Debug|x86.Build.0 = Debug|Any CPU
{AABDE67A-1EB9-4B52-B8E3-48D483E1F026}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AABDE67A-1EB9-4B52-B8E3-48D483E1F026}.Release|Any CPU.Build.0 = Release|Any CPU
{AABDE67A-1EB9-4B52-B8E3-48D483E1F026}.Release|x64.ActiveCfg = Release|Any CPU
{AABDE67A-1EB9-4B52-B8E3-48D483E1F026}.Release|x64.Build.0 = Release|Any CPU
{AABDE67A-1EB9-4B52-B8E3-48D483E1F026}.Release|x86.ActiveCfg = Release|Any CPU
{AABDE67A-1EB9-4B52-B8E3-48D483E1F026}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using InvestmentPerformanceWebAPI.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace InvestmentPerformanceWebAPI.Controllers
{
[ApiController]
[Route("api/v1/users/{userId}/[controller]")]
[ApiVersion("1.0")]
public class InvestmentController : ControllerBase
{
private readonly InvestmentDbContext _context;
private readonly ILogger<InvestmentController> _logger;

public InvestmentController(InvestmentDbContext context, ILogger<InvestmentController> logger)
{
_context = context;
_logger = logger;
}

// GET: api/v1/users/{userId}/investments
[HttpGet]
public async Task<ActionResult<IEnumerable<object>>> GetInvestmentsForUser(int userId)
{
try
{
_logger.LogInformation("Fetching investments for user {UserId}", userId);

var user = await _context.Users
.Include(u => u.Investments)
.FirstOrDefaultAsync(u => u.UserId == userId);

if (user == null)
{
_logger.LogWarning("User {UserId} not found", userId);
return NotFound($"User {userId} not found");
}

var investments = user.Investments.Select(i => new
{
i.InvestmentId,
i.Name
});

return Ok(investments);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error occurred when fetching investments for user {userId}", userId);
return StatusCode(500, "An unepected error occurred, please try again later.");
}

}

// GET: api/v1/users/{userId}/investments/{investmentId}
[HttpGet("{investmentId}")]
public async Task<ActionResult<object>> GetInvestmentDetails(int userId, int investmentId)
{
try
{
_logger.LogInformation("Fetching details for investment {investmentId} for user {userId}", investmentId, userId);

var investment = await _context.Investments
.Where(i => i.UserId == userId && i.InvestmentId == investmentId)
.FirstOrDefaultAsync();

if (investment == null)
{
_logger.LogWarning("Investment {investmentId} not found for user {userId}", investmentId, userId);
return NotFound($"Investment {investmentId} not found for user {userId}");
}

var result = new
{
investment.InvestmentId,
investment.Name,
investment.Ticker,
investment.NumberOfShares,
investment.CostBasisPerShare,
investment.CurrentPrice,
investment.CurrentValue,
investment.Term,
investment.TotalGainLoss
};

return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error occurred when fetching investment {investmentId} details for user {userId}", investmentId, userId);
return StatusCode(500, "An unepected error occurred, please try again later.");
}

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Microsoft.EntityFrameworkCore;
using InvestmentPerformanceWebAPI.Models;

namespace InvestmentPerformanceWebAPI.Data
{
public class InvestmentDbContext : DbContext
{
public InvestmentDbContext(DbContextOptions<InvestmentDbContext> options) : base(options)
{

}

public DbSet<User> Users => Set<User>();
public DbSet<Investment> Investments => Set<Investment>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasMany(u => u.Investments)
.WithOne(i => i.User!)
.HasForeignKey(i => i.UserId);

modelBuilder.Entity<User>().HasData(
new User { UserId = 1, Name = "Alice" },
new User { UserId = 2, Name = "Bob" }
);

modelBuilder.Entity<Investment>().HasData(
new Investment
{
InvestmentId = 1,
UserId = 1,
Name = "Apple Inc.",
Ticker = "AAPL",
NumberOfShares = 10,
CostBasisPerShare = 120.00m,
CurrentPrice = 175.00m,
TimeOfPurchase = new DateTime(2023, 6, 10)
},
new Investment
{
InvestmentId = 2,
UserId = 1,
Name = "Tesla Motors",
Ticker = "TSLA",
NumberOfShares = 5,
CostBasisPerShare = 220.00m,
CurrentPrice = 200.00m,
TimeOfPurchase = new DateTime(2025, 9, 15)
},
new Investment
{
InvestmentId = 3,
UserId = 2,
Name = "Amazon.com",
Ticker = "AMZN",
NumberOfShares = 8,
CostBasisPerShare = 95.00m,
CurrentPrice = 130.00m,
TimeOfPurchase = new DateTime(2024, 2, 1)
}
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.1.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.10">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
</ItemGroup>

</Project>
Loading