Skip to content
Merged
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
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ riderModule.iml
/node_modules/**

# Generated GraphQL schema (generated during build)
frontend/schema.graphql
frontend/schema.graphql

# Test Results
TestResults/
CoverageReport/
*.trx
*.coverage
10 changes: 10 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,15 @@
<!-- File Processing -->
<PackageVersion Include="CsvHelper" Version="33.1.0" />
<PackageVersion Include="EPPlus" Version="8.2.1" />

<!-- Testing -->
<PackageVersion Include="xunit" Version="2.9.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="FluentAssertions" Version="7.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.0" />
</ItemGroup>
</Project>
9 changes: 6 additions & 3 deletions PhantomDave.BankTracking.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,13 @@ public static void Main(string[] args)

var app = builder.Build();

using (var scope = app.Services.CreateScope())
if (!app.Environment.IsEnvironment("Testing"))
{
var dbContext = scope.ServiceProvider.GetRequiredService<BankTrackerDbContext>();
dbContext.Database.Migrate();
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<BankTrackerDbContext>();
dbContext.Database.Migrate();
}
}

app.UseCors();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using FluentAssertions;
using PhantomDave.BankTracking.IntegrationTests.Helpers;

namespace PhantomDave.BankTracking.IntegrationTests.GraphQL;

public class AccountIntegrationTests : IClassFixture<GraphQLTestFactory>
{
private readonly HttpClient _client;
private readonly GraphQLTestFactory _factory;

public AccountIntegrationTests(GraphQLTestFactory factory)
{
_factory = factory;
Comment on lines +12 to +16
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _factory field is stored but never used in any test methods. This field can be removed since only _client is used throughout the test class.

Suggested change
private readonly GraphQLTestFactory _factory;
public AccountIntegrationTests(GraphQLTestFactory factory)
{
_factory = factory;
public AccountIntegrationTests(GraphQLTestFactory factory)
{

Copilot uses AI. Check for mistakes.
_client = factory.CreateClient();
}

[Fact]
public async Task GraphQL_Endpoint_IsAccessible()
{
// Arrange & Act
var response = await _client.GetAsync("/graphql?sdl");

// Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
content.Should().Contain("type Query");
}

[Fact]
public async Task CreateAccount_WithValidData_ReturnsSuccess()
{
// Arrange
var query = @"
mutation {
createAccount(email: ""test@example.com"", password: ""Password123!"") {
id
email
createdAt
}
}";
Comment on lines +36 to +43
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GraphQL query string is duplicated across multiple tests with only minor variations (email addresses). Consider extracting common query patterns into reusable helper methods or constants to follow the DRY principle. For example:

private static string CreateAccountMutation(string email, string password) =>
    $@"mutation {{
        createAccount(email: ""{email}"", password: ""{password}"") {{
            id
            email
            createdAt
        }}
    }}";

Copilot uses AI. Check for mistakes.

var request = new
{
query = query
};

// Act
var response = await _client.PostAsJsonAsync("/graphql", request);

// Assert
response.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();

if (content.Contains("errors"))
{
content.Should().Contain("test@example.com", "Even with errors, successful account creation should return email");
}
else
{
content.Should().Contain("test@example.com");
content.Should().Contain("\"id\"");
}
}

[Fact]
public async Task CreateAccount_WithDuplicateEmail_ReturnsError()
{
// Arrange
var createQuery = @"
mutation {
createAccount(email: ""duplicate@example.com"", password: ""Password123!"") {
id
email
}
}";

await _client.PostAsJsonAsync("/graphql", new { query = createQuery });
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The result of the first account creation is not checked or stored. If the first creation fails, the test may produce a false positive. Consider storing the response and verifying it succeeded before proceeding to test the duplicate email scenario.

Suggested change
await _client.PostAsJsonAsync("/graphql", new { query = createQuery });
var firstResponse = await _client.PostAsJsonAsync("/graphql", new { query = createQuery });
firstResponse.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
var firstContent = await firstResponse.Content.ReadAsStringAsync();
firstContent.Should().NotContain("errors", "First account creation should succeed to properly test duplicate scenario");

Copilot uses AI. Check for mistakes.

// Act
var response = await _client.PostAsJsonAsync("/graphql", new { query = createQuery });

// Assert
var content = await response.Content.ReadAsStringAsync();
content.Should().Contain("errors");
}

[Fact]
public async Task Login_WithValidCredentials_ReturnsToken()
{
// Arrange
var email = "login@example.com";
var password = "Password123!";

var createQuery = $@"
mutation {{
createAccount(email: ""{email}"", password: ""{password}"") {{
id
}}
}}";

await _client.PostAsJsonAsync("/graphql", new { query = createQuery });
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The account creation result is not checked before attempting login. If account creation fails, the login test will also fail but for the wrong reason, making debugging harder. Consider verifying the creation response before proceeding.

Suggested change
await _client.PostAsJsonAsync("/graphql", new { query = createQuery });
var createResponse = await _client.PostAsJsonAsync("/graphql", new { query = createQuery });
var createContent = await createResponse.Content.ReadAsStringAsync();
createResponse.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
createContent.Should().NotContain("\"errors\"", "Account creation should succeed before attempting login");
createContent.Should().Contain("\"id\"", "Account creation response should contain an id");

Copilot uses AI. Check for mistakes.

var loginQuery = $@"
mutation {{
loginAccount(email: ""{email}"", password: ""{password}"") {{
token
account {{
id
email
}}
}}
}}";

// Act
var response = await _client.PostAsJsonAsync("/graphql", new { query = loginQuery });

// Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
content.Should().Contain("token");
content.Should().Contain(email);
}

[Fact]
public async Task Login_WithInvalidPassword_ReturnsError()
{
// Arrange
var email = "wrongpass@example.com";
var correctPassword = "CorrectPassword123!";
var wrongPassword = "WrongPassword123!";

var createQuery = $@"
mutation {{
createAccount(email: ""{email}"", password: ""{correctPassword}"") {{
id
}}
}}";

await _client.PostAsJsonAsync("/graphql", new { query = createQuery });

var loginQuery = $@"
mutation {{
loginAccount(email: ""{email}"", password: ""{wrongPassword}"") {{
token
}}
}}";

// Act
var response = await _client.PostAsJsonAsync("/graphql", new { query = loginQuery });

// Assert
var content = await response.Content.ReadAsStringAsync();
content.Should().Contain("errors");
}

[Fact]
public async Task VerifyToken_WithValidToken_ReturnsAccountInfo()
{
// Arrange
var email = "verify@example.com";
var password = "Password123!";

var createQuery = $@"
mutation {{
createAccount(email: ""{email}"", password: ""{password}"") {{
id
}}
}}";

await _client.PostAsJsonAsync("/graphql", new { query = createQuery });

var loginQuery = $@"
mutation {{
loginAccount(email: ""{email}"", password: ""{password}"") {{
token
}}
}}";

var loginResponse = await _client.PostAsJsonAsync("/graphql", new { query = loginQuery });
var loginContent = await loginResponse.Content.ReadAsStringAsync();

using var doc = JsonDocument.Parse(loginContent);
var token = doc.RootElement.GetProperty("data").GetProperty("loginAccount").GetProperty("token").GetString();
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential NullReferenceException if the JSON parsing fails or the expected properties don't exist in the response. The code attempts to access nested properties without null checking. Add proper error handling or use TryGetProperty to safely navigate the JSON structure, especially since some tests acknowledge that GraphQL schema issues may cause unexpected responses.

Suggested change
var token = doc.RootElement.GetProperty("data").GetProperty("loginAccount").GetProperty("token").GetString();
if (!doc.RootElement.TryGetProperty("data", out var dataElem))
throw new Xunit.Sdk.XunitException("Response JSON does not contain 'data' property: " + loginContent);
if (!dataElem.TryGetProperty("loginAccount", out var loginAccountElem))
throw new Xunit.Sdk.XunitException("Response JSON does not contain 'loginAccount' property: " + loginContent);
if (!loginAccountElem.TryGetProperty("token", out var tokenElem))
throw new Xunit.Sdk.XunitException("Response JSON does not contain 'token' property: " + loginContent);
var token = tokenElem.GetString();

Copilot uses AI. Check for mistakes.

var verifyQuery = $@"
mutation {{
verifyToken(token: ""{token}"") {{
id
email
}}
}}";

// Act
var response = await _client.PostAsJsonAsync("/graphql", new { query = verifyQuery });

// Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
content.Should().Contain(email);
}

[Fact]
public async Task GetAccount_WithAuthentication_ReturnsAccountData()
{
// Arrange
var email = "getaccount@example.com";
var password = "Password123!";

var createQuery = $@"
mutation {{
createAccount(email: ""{email}"", password: ""{password}"") {{
id
}}
}}";

await _client.PostAsJsonAsync("/graphql", new { query = createQuery });

var loginQuery = $@"
mutation {{
loginAccount(email: ""{email}"", password: ""{password}"") {{
token
account {{
id
}}
}}
}}";

var loginResponse = await _client.PostAsJsonAsync("/graphql", new { query = loginQuery });
var loginContent = await loginResponse.Content.ReadAsStringAsync();

using var doc = JsonDocument.Parse(loginContent);
var token = doc.RootElement.GetProperty("data").GetProperty("loginAccount").GetProperty("token").GetString();
var accountId = doc.RootElement.GetProperty("data").GetProperty("loginAccount").GetProperty("account").GetProperty("id").GetInt32();
Comment on lines +234 to +236
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as lines 185-186: potential NullReferenceException when parsing the JSON response without null checking. The nested property access could fail if the response structure is unexpected.

Copilot uses AI. Check for mistakes.

_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);

var getAccountQuery = $@"
query {{
getAccount(id: {accountId}) {{
id
email
currentBalance
}}
}}";

// Act
var response = await _client.PostAsJsonAsync("/graphql", new { query = getAccountQuery });

// Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
content.Should().Contain(email);
content.Should().Contain("currentBalance");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using PhantomDave.BankTracking.Data.Context;

namespace PhantomDave.BankTracking.IntegrationTests.Helpers;

public class GraphQLTestFactory : WebApplicationFactory<PhantomDave.BankTracking.Api.Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration((context, config) =>
{
config.AddInMemoryCollection(new Dictionary<string, string?>
{
["ConnectionStrings:DefaultConnection"] = "Host=localhost;Database=banktrackertest;Username=test;Password=test",
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The connection string is configured but never used since the DbContext is replaced with an in-memory database (line 40). This configuration entry can be removed to avoid confusion.

Suggested change
["ConnectionStrings:DefaultConnection"] = "Host=localhost;Database=banktrackertest;Username=test;Password=test",

Copilot uses AI. Check for mistakes.
["Jwt:Secret"] = "ThisIsASecretKeyForTestingPurposesOnly123456789",
["Jwt:Issuer"] = "BankTrackerTestIssuer",
["Jwt:Audience"] = "BankTrackerTestAudience",
["Jwt:ExpiryMinutes"] = "60"
});
});

builder.ConfigureTestServices(services =>
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<BankTrackerDbContext>));

if (descriptor != null)
{
services.Remove(descriptor);
}

services.AddDbContext<BankTrackerDbContext>(options =>
{
options.UseInMemoryDatabase("InMemoryTestDb");
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The in-memory database name "InMemoryTestDb" is shared across all tests. Since xUnit runs tests in parallel by default, this could lead to race conditions where tests interfere with each other's data. Consider using a unique database name per test instance (e.g., using a GUID) to ensure proper test isolation:

options.UseInMemoryDatabase($"InMemoryTestDb_{Guid.NewGuid()}");

Copilot uses AI. Check for mistakes.
});
});

builder.UseEnvironment("Testing");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup>
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
</ItemGroup>

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

<ItemGroup>
<ProjectReference Include="..\PhantomDave.BankTracking.Api\PhantomDave.BankTracking.Api.csproj" />
</ItemGroup>

</Project>
Loading
Loading