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
25 changes: 25 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Build outputs
bin/
obj/

# User-specific files
*.user
*.suo

# Visual Studio
.vs/

# NuGet packages
packages/

# Environment files
.env
appsettings.local.json

# Test results
TestResults/
coverage/

# OS files
.DS_Store
Thumbs.db
28 changes: 28 additions & 0 deletions CodingExercise.Tests/CodingExercise.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<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="FluentAssertions" Version="8.8.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" 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="..\src\CodingExercise.csproj" />
</ItemGroup>

</Project>
169 changes: 169 additions & 0 deletions CodingExercise.Tests/InvestmentServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
using Microsoft.Extensions.Logging;
using FluentAssertions;
using Moq;
using Xunit;
using CodingExercise.Services;
using CodingExercise.Models;

namespace CodingExercise.Tests;

public class InvestmentServiceTests
{
private readonly Mock<ILogger<InvestmentService>> _mockLogger;
private readonly InvestmentService _service;

public InvestmentServiceTests()
{
_mockLogger = new Mock<ILogger<InvestmentService>>();
_service = new InvestmentService(_mockLogger.Object);
}

// Tests that the service returns investment data for a valid existing user.
// Should return a non-empty list with valid investment summaries.
[Fact]
public async Task GetUserInvestments_ExistingUser_ReturnsInvestments()
{
var userId = "user1";

var result = await _service.GetUserInvestments(userId);

result.Should().NotBeEmpty();
result.Should().AllSatisfy(investment =>
{
investment.Id.Should().BePositive();
investment.Name.Should().NotBeNullOrEmpty();
});
}

// Tests that the service returns an empty list for users that don't exist.
// Should return an empty collection rather than throwing an exception.
[Fact]
public async Task GetUserInvestments_NonExistentUser_ReturnsEmptyList()
{
var userId = "nonexistentuser";

var result = await _service.GetUserInvestments(userId);

result.Should().BeEmpty();
}

// Tests that user ID matching is case-insensitive.
// Should return same results for "user1" and "USER1".
[Fact]
public async Task GetUserInvestments_CaseInsensitive_ReturnsInvestments()
{
var userId = "USER1"; // Uppercase version

var result = await _service.GetUserInvestments(userId);

result.Should().NotBeEmpty();
}

// Tests that the service returns complete investment details for valid investment IDs.
[Fact]
public async Task GetInvestmentDetails_ExistingInvestment_ReturnsDetails()
{
var investmentId = 1; // Apple investment

var result = await _service.GetInvestmentDetails(investmentId);

result.Should().NotBeNull();
result!.Id.Should().Be(investmentId);
result.Name.Should().Be("Apple");
result.Shares.Should().BePositive();
result.CostBasisPerShare.Should().BePositive();
result.CurrentPrice.Should().BePositive();
result.CurrentValue.Should().BePositive();
result.Term.Should().BeOneOf("Short Term", "Long Term");
}

// Tests that the service returns null for investment IDs that don't exist.
[Fact]
public async Task GetInvestmentDetails_NonExistentInvestment_ReturnsNull()
{
var investmentId = 999;

var result = await _service.GetInvestmentDetails(investmentId);

result.Should().BeNull();
}

// Tests that investments with gains show positive total gain/loss values.
[Theory]
[InlineData(1)] // Apple - should be profitable
[InlineData(3)] // Google - should be profitable
[InlineData(5)] // Meta - should be profitable
public async Task GetInvestmentDetails_ProfitableInvestments_HasPositiveGains(int investmentId)
{
var result = await _service.GetInvestmentDetails(investmentId);

result.Should().NotBeNull();
result!.TotalGainLoss.Should().BePositive();
result.CurrentValue.Should().BeGreaterThan(result.Shares * result.CostBasisPerShare);
}

// Tests that investments with losses show negative total gain/loss values.
[Theory]
[InlineData(2)] // Microsoft - should be loss
[InlineData(4)] // Tesla - should be loss
public async Task GetInvestmentDetails_LossInvestments_HasNegativeGains(int investmentId)
{
var result = await _service.GetInvestmentDetails(investmentId);

result.Should().NotBeNull();
result!.TotalGainLoss.Should().BeNegative();
result.CurrentValue.Should().BeLessThan(result.Shares * result.CostBasisPerShare);
}

// Tests that current value calculation is correct (shares × current price).
[Fact]
public async Task GetInvestmentDetails_CalculatesCurrentValueCorrectly()
{
var investmentId = 1; // Apple investment

var result = await _service.GetInvestmentDetails(investmentId);

result.Should().NotBeNull();
var expectedCurrentValue = result!.Shares * result.CurrentPrice;
result.CurrentValue.Should().Be(expectedCurrentValue);
}

// Tests that total gain/loss calculation is correct (current value - total cost).
[Fact]
public async Task GetInvestmentDetails_CalculatesTotalGainLossCorrectly()
{
var investmentId = 1; // Apple investment

var result = await _service.GetInvestmentDetails(investmentId);

result.Should().NotBeNull();
var totalCost = result!.Shares * result.CostBasisPerShare;
var expectedGainLoss = result.CurrentValue - totalCost;
result.TotalGainLoss.Should().Be(expectedGainLoss);
}

// Tests that investments held for more than 365 days are classified as "Long Term".
[Theory]
[InlineData(1)] // Apple - Long term (400+ days)
[InlineData(3)] // Google - Long term (600+ days)
public async Task GetInvestmentDetails_LongTermInvestments_ReturnsLongTerm(int investmentId)
{
var result = await _service.GetInvestmentDetails(investmentId);

result.Should().NotBeNull();
result!.Term.Should().Be("Long Term");
}

// Tests that investments held for 365 days or less are classified as "Short Term".
[Theory]
[InlineData(2)] // Microsoft - Short term (200 days)
[InlineData(4)] // Tesla - Short term (150 days)
[InlineData(5)] // Meta - Short term (30 days)
public async Task GetInvestmentDetails_ShortTermInvestments_ReturnsShortTerm(int investmentId)
{
var result = await _service.GetInvestmentDetails(investmentId);

result.Should().NotBeNull();
result!.Term.Should().Be("Short Term");
}
}
Loading