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
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
################################################################################
# This .gitignore file was automatically created by Microsoft(R) Visual Studio.
################################################################################

/.vs
/InvestmentPerformanceWebApi/.vs/InvestmentPerformanceWebApi
/InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/bin/Debug/net8.0
/InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/obj
/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/bin/Debug/net8.0
/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/obj
/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/app.db
/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/app.db-shm
/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/app.db-wal
8 changes: 6 additions & 2 deletions InvestmentPerformanceWebAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
## General instructions:
Please fork this project and submit a pull request when completed. You should submit a working piece of software that is tested and can be run. We will review your pull request and execute your code, please provide instructions on how to do so.

Please keep in mind that this exercise is intended to be achievable in a couple of hours. We expect a production ready submission that demonstrates not only the code you write but quality controls such as exception handling, logging, unit tests, etc. Assume that this api will be part of a larger system. If there are larger considerations, that would have affected decisions of what is in/out of scope, please make note of your assumptions. The majority of the tech stack in use at Nuix Discover is SQLServer, C#.NET, ExtJs, Vue.js. We prefer the use of that stack, but we want you to showcase your abilities. If you don't know those specific technologies, you can substitute.
Please keep in mind that this exercise is intended to be achievable in a couple of hours. We expect a production ready submission that demonstrates not only the code you write but quality controls such as exception handling,
logging, unit tests, etc. Assume that this api will be part of a larger system. If there are larger considerations, that would have affected decisions of what is in/out of scope, please make note of your assumptions.
The majority of the tech stack in use at Nuix Discover is SQLServer, C#.NET, ExtJs, Vue.js. We prefer the use of that stack, but we want you to showcase your abilities. If you don't know those specific technologies,
you can substitute.



## Problem statement
The company you are working for is building an investment trading platform for stock, bond and mutual funds. The platform will have various functionality to buy, sell, deposit, withdrawal, and report on investments. For this part of the product, they need you to create a web api to return data for investment performance. The type of api to create is your decision, but it must be hosted on a webserver and accessed via web request.
The company you are working for is building an investment trading platform for stock, bond and mutual funds. The platform will have various functionality to buy, sell, deposit, withdrawal, and report on investments.
For this part of the product, they need you to create a web api to return data for investment performance. The type of api to create is your decision, but it must be hosted on a webserver and accessed via web request.

## Problem details/user stories:
- ### Get a list of current investments for the user
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using Moq;
using AutoMapper;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.AspNetCore.Mvc;
using AutoFixture;
using InvestmentPerformanceWebApi.Api.Controllers;
using InvestmentPerformanceWebApi.Services.Interfaces;
using InvestmentPerformanceWebApi.Api.ViewModels.Investments;

namespace InvestmentPerformanceApi.Tests.ControllerTests
{
public class UserInvestmentsControllerTests
{
private readonly Mock<IInvestmentService> _mockInvestmentService;
private readonly IMapper _mapper;
private readonly Mock<ILogger<UserInvestmentsController>> _mockLogger;
private readonly UserInvestmentsController _controller;

public UserInvestmentsControllerTests()
{
_mockInvestmentService = new Mock<IInvestmentService>();
_mockLogger = new Mock<ILogger<UserInvestmentsController>>();

var configExpression = new MapperConfigurationExpression();
configExpression.CreateMap<InvestmentPerformanceWebApi.Domain.Models.Investment, InvestmentSummaryView>();
configExpression.CreateMap<InvestmentPerformanceWebApi.Domain.Models.Investment, InvestmentDetailView>();

var mapperConfig = new MapperConfiguration(configExpression, NullLoggerFactory.Instance);
_mapper = mapperConfig.CreateMapper();

_controller = new UserInvestmentsController(
_mockLogger.Object,
_mockInvestmentService.Object,
_mapper
);
}

[Fact]
public async Task GetInvestmentsForUser_ReturnsOk_WithMappedDto()
{
// Arrange
int userId = 1;
var fixture = new Fixture();
var investments = fixture.CreateMany<InvestmentPerformanceWebApi.Domain.Models.Investment>(3);


// Use Task.FromResult to avoid Moq ReturnsAsync issues
_mockInvestmentService
.Setup(s => s.GetInvestmentsForUserAsync(userId))
.Returns(Task.FromResult(investments));
// Act
var result = await _controller.GetInvestmentsForUser(userId);

// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var returnedDtos = Assert.IsAssignableFrom<IEnumerable<InvestmentSummaryView>>(okResult.Value);
Assert.Equal(3, ((List<InvestmentSummaryView>)returnedDtos).Count);
}
[Fact]
public async Task GetSingleInvestmentForUser_ReturnsOk_WithMappedDto()
{
// Arrange
int userId = 1;

var fixture = new Fixture();

var investment = fixture.Create<InvestmentPerformanceWebApi.Domain.Models.Investment>();

_mockInvestmentService
.Setup(s => s.GetInvestmentForUserAsync(userId, investment.Id))
.Returns(Task.FromResult(investment));

// Act
var result = await _controller.GetInvestmentForUser(userId, investment.Id);

// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);

var dto = Assert.IsType<InvestmentDetailView>(okResult.Value);

Assert.Equal(investment.Id, dto.Id);
Assert.Equal(investment.Name, dto.Name);
}


[Fact]
public async Task GetInvestmentsForUser_ReturnsNotFound_WhenInvestmentDoesNotExist()
{
// Arrange

_mockInvestmentService
.Setup(s => s.GetInvestmentsForUserAsync(It.IsAny<int>()))
.ThrowsAsync(new KeyNotFoundException("Investment not found"));

// Act
var result = await _controller.GetInvestmentsForUser(1);

// Assert
var notFoundResult = Assert.IsType<NotFoundObjectResult>(result.Result);

Assert.Contains("Investment not found", notFoundResult.Value.ToString());
}

[Fact]
public async Task GetSingleInvestmentsForUser_ReturnsNotFound_WhenInvestmentDoesNotExist()
{
// Arrange
_mockInvestmentService
.Setup(s => s.GetInvestmentForUserAsync(It.IsAny<int>(), It.IsAny<int>()))
.ThrowsAsync(new KeyNotFoundException("Investment not found"));

// Act
var result = await _controller.GetInvestmentForUser(1, 2);

// Assert
var notFoundResult = Assert.IsType<NotFoundObjectResult>(result.Result);
Assert.Contains("Investment not found", notFoundResult.Value.ToString());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using System;
using InvestmentPerformanceWebApi.Domain.Models;
using Xunit;

namespace InvestmentPerformanceWebApi.Tests.Domain
{
public class InvestmentTests
{
[Fact]
public void Constructor_WithNegativeShares_ThrowsArgumentException()
{
// Arrange
string name = "Test Investment";
int id = 1;
int shares = -5;
decimal costBasis = 100m;
decimal currentPrice = 120m;
DateTime purchasedDate = DateTime.UtcNow;

// Act & Assert
Assert.Throws<ArgumentException>(() =>
new Investment(name, id, shares, costBasis, currentPrice, purchasedDate));
}

[Fact]
public void CurrentValue_ComputesCorrectly()
{
// Arrange
var investment = new Investment(
"Test",
1,
10,
50m,
60m,
DateTime.UtcNow.AddDays(-10));

// Act
var value = investment.CurrentValue;

// Assert
Assert.Equal(10 * 60m, value);
}

[Theory]
[InlineData(-10, "Short Term")]
[InlineData(-400, "Long Term")]
[InlineData(365, "Short Term")]
[InlineData(1, "Short Term")]
//[InlineData(100000000000, "Long Term")] //chokes on this, would need to adjust data rules for how long someone can hold an investment for
public void Term_ReturnsCorrectValue(int daysOffset, string expectedTerm)
{
// Arrange
var purchasedDate = DateTime.UtcNow.AddDays(daysOffset);
var investment = new Investment(
"Test",
1,
10,
50m,
60m,
purchasedDate);

// Act
var term = investment.Term;

// Assert
Assert.Equal(expectedTerm, term);
}

[Fact]
public void TotalGainOrLoss_ComputesCorrectly()
{
// Arrange
var investment = new Investment(
"Test",
1,
10,
50m,
60m,
DateTime.UtcNow.AddDays(-10));

// Act
var totalGain = investment.TotalGainOrLoss;

// Assert
Assert.Equal((10 * 60m) - (10 * 50m), totalGain);
}

[Fact]
public void Constructor_InitializesPropertiesCorrectly()
{
// Arrange
string name = "Test Investment";
int id = 1;
int shares = 10;
decimal costBasis = 50m;
decimal currentPrice = 60m;
DateTime purchasedDate = DateTime.UtcNow;

// Act
var investment = new Investment(name, id, shares, costBasis, currentPrice, purchasedDate);

// Assert
Assert.Equal(name, investment.Name);
Assert.Equal(id, investment.Id);
Assert.Equal(shares, investment.Shares);
Assert.Equal(costBasis, investment.CostBasis);
Assert.Equal(currentPrice, investment.CurrentPrice);
Assert.Equal(purchasedDate, investment.PurchasedUtc);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.18.1" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageReference Include="AutoMapper" Version="16.0.0" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.22" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>

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

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

</Project>
Loading