Skip to content

Commit 98e157e

Browse files
CopilotPhantomDave
andauthored
Add comprehensive testing infrastructure with xUnit, Playwright, and WebApplicationFactory (#74)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: PhantomDave <34485699+PhantomDave@users.noreply.github.com>
1 parent 2669f35 commit 98e157e

File tree

19 files changed

+1774
-10
lines changed

19 files changed

+1774
-10
lines changed

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,10 @@ riderModule.iml
77
/node_modules/**
88

99
# Generated GraphQL schema (generated during build)
10-
frontend/schema.graphql
10+
frontend/schema.graphql
11+
12+
# Test Results
13+
TestResults/
14+
CoverageReport/
15+
*.trx
16+
*.coverage

Directory.Packages.props

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,15 @@
2525
<!-- File Processing -->
2626
<PackageVersion Include="CsvHelper" Version="33.1.0" />
2727
<PackageVersion Include="EPPlus" Version="8.2.1" />
28+
29+
<!-- Testing -->
30+
<PackageVersion Include="xunit" Version="2.9.2" />
31+
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.0" />
32+
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
33+
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
34+
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" />
35+
<PackageVersion Include="Moq" Version="4.20.72" />
36+
<PackageVersion Include="FluentAssertions" Version="7.0.0" />
37+
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.0" />
2838
</ItemGroup>
2939
</Project>

PhantomDave.BankTracking.Api/Program.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,13 @@ public static void Main(string[] args)
103103

104104
var app = builder.Build();
105105

106-
using (var scope = app.Services.CreateScope())
106+
if (!app.Environment.IsEnvironment("Testing"))
107107
{
108-
var dbContext = scope.ServiceProvider.GetRequiredService<BankTrackerDbContext>();
109-
dbContext.Database.Migrate();
108+
using (var scope = app.Services.CreateScope())
109+
{
110+
var dbContext = scope.ServiceProvider.GetRequiredService<BankTrackerDbContext>();
111+
dbContext.Database.Migrate();
112+
}
110113
}
111114

112115
app.UseCors();
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
using System.Net.Http.Json;
2+
using System.Text;
3+
using System.Text.Json;
4+
using FluentAssertions;
5+
using PhantomDave.BankTracking.IntegrationTests.Helpers;
6+
7+
namespace PhantomDave.BankTracking.IntegrationTests.GraphQL;
8+
9+
public class AccountIntegrationTests : IClassFixture<GraphQLTestFactory>
10+
{
11+
private readonly HttpClient _client;
12+
private readonly GraphQLTestFactory _factory;
13+
14+
public AccountIntegrationTests(GraphQLTestFactory factory)
15+
{
16+
_factory = factory;
17+
_client = factory.CreateClient();
18+
}
19+
20+
[Fact]
21+
public async Task GraphQL_Endpoint_IsAccessible()
22+
{
23+
// Arrange & Act
24+
var response = await _client.GetAsync("/graphql?sdl");
25+
26+
// Assert
27+
response.EnsureSuccessStatusCode();
28+
var content = await response.Content.ReadAsStringAsync();
29+
content.Should().Contain("type Query");
30+
}
31+
32+
[Fact]
33+
public async Task CreateAccount_WithValidData_ReturnsSuccess()
34+
{
35+
// Arrange
36+
var query = @"
37+
mutation {
38+
createAccount(email: ""test@example.com"", password: ""Password123!"") {
39+
id
40+
email
41+
createdAt
42+
}
43+
}";
44+
45+
var request = new
46+
{
47+
query = query
48+
};
49+
50+
// Act
51+
var response = await _client.PostAsJsonAsync("/graphql", request);
52+
53+
// Assert
54+
response.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
55+
var content = await response.Content.ReadAsStringAsync();
56+
57+
if (content.Contains("errors"))
58+
{
59+
content.Should().Contain("test@example.com", "Even with errors, successful account creation should return email");
60+
}
61+
else
62+
{
63+
content.Should().Contain("test@example.com");
64+
content.Should().Contain("\"id\"");
65+
}
66+
}
67+
68+
[Fact]
69+
public async Task CreateAccount_WithDuplicateEmail_ReturnsError()
70+
{
71+
// Arrange
72+
var createQuery = @"
73+
mutation {
74+
createAccount(email: ""duplicate@example.com"", password: ""Password123!"") {
75+
id
76+
email
77+
}
78+
}";
79+
80+
await _client.PostAsJsonAsync("/graphql", new { query = createQuery });
81+
82+
// Act
83+
var response = await _client.PostAsJsonAsync("/graphql", new { query = createQuery });
84+
85+
// Assert
86+
var content = await response.Content.ReadAsStringAsync();
87+
content.Should().Contain("errors");
88+
}
89+
90+
[Fact]
91+
public async Task Login_WithValidCredentials_ReturnsToken()
92+
{
93+
// Arrange
94+
var email = "login@example.com";
95+
var password = "Password123!";
96+
97+
var createQuery = $@"
98+
mutation {{
99+
createAccount(email: ""{email}"", password: ""{password}"") {{
100+
id
101+
}}
102+
}}";
103+
104+
await _client.PostAsJsonAsync("/graphql", new { query = createQuery });
105+
106+
var loginQuery = $@"
107+
mutation {{
108+
loginAccount(email: ""{email}"", password: ""{password}"") {{
109+
token
110+
account {{
111+
id
112+
email
113+
}}
114+
}}
115+
}}";
116+
117+
// Act
118+
var response = await _client.PostAsJsonAsync("/graphql", new { query = loginQuery });
119+
120+
// Assert
121+
response.EnsureSuccessStatusCode();
122+
var content = await response.Content.ReadAsStringAsync();
123+
content.Should().Contain("token");
124+
content.Should().Contain(email);
125+
}
126+
127+
[Fact]
128+
public async Task Login_WithInvalidPassword_ReturnsError()
129+
{
130+
// Arrange
131+
var email = "wrongpass@example.com";
132+
var correctPassword = "CorrectPassword123!";
133+
var wrongPassword = "WrongPassword123!";
134+
135+
var createQuery = $@"
136+
mutation {{
137+
createAccount(email: ""{email}"", password: ""{correctPassword}"") {{
138+
id
139+
}}
140+
}}";
141+
142+
await _client.PostAsJsonAsync("/graphql", new { query = createQuery });
143+
144+
var loginQuery = $@"
145+
mutation {{
146+
loginAccount(email: ""{email}"", password: ""{wrongPassword}"") {{
147+
token
148+
}}
149+
}}";
150+
151+
// Act
152+
var response = await _client.PostAsJsonAsync("/graphql", new { query = loginQuery });
153+
154+
// Assert
155+
var content = await response.Content.ReadAsStringAsync();
156+
content.Should().Contain("errors");
157+
}
158+
159+
[Fact]
160+
public async Task VerifyToken_WithValidToken_ReturnsAccountInfo()
161+
{
162+
// Arrange
163+
var email = "verify@example.com";
164+
var password = "Password123!";
165+
166+
var createQuery = $@"
167+
mutation {{
168+
createAccount(email: ""{email}"", password: ""{password}"") {{
169+
id
170+
}}
171+
}}";
172+
173+
await _client.PostAsJsonAsync("/graphql", new { query = createQuery });
174+
175+
var loginQuery = $@"
176+
mutation {{
177+
loginAccount(email: ""{email}"", password: ""{password}"") {{
178+
token
179+
}}
180+
}}";
181+
182+
var loginResponse = await _client.PostAsJsonAsync("/graphql", new { query = loginQuery });
183+
var loginContent = await loginResponse.Content.ReadAsStringAsync();
184+
185+
using var doc = JsonDocument.Parse(loginContent);
186+
var token = doc.RootElement.GetProperty("data").GetProperty("loginAccount").GetProperty("token").GetString();
187+
188+
var verifyQuery = $@"
189+
mutation {{
190+
verifyToken(token: ""{token}"") {{
191+
id
192+
email
193+
}}
194+
}}";
195+
196+
// Act
197+
var response = await _client.PostAsJsonAsync("/graphql", new { query = verifyQuery });
198+
199+
// Assert
200+
response.EnsureSuccessStatusCode();
201+
var content = await response.Content.ReadAsStringAsync();
202+
content.Should().Contain(email);
203+
}
204+
205+
[Fact]
206+
public async Task GetAccount_WithAuthentication_ReturnsAccountData()
207+
{
208+
// Arrange
209+
var email = "getaccount@example.com";
210+
var password = "Password123!";
211+
212+
var createQuery = $@"
213+
mutation {{
214+
createAccount(email: ""{email}"", password: ""{password}"") {{
215+
id
216+
}}
217+
}}";
218+
219+
await _client.PostAsJsonAsync("/graphql", new { query = createQuery });
220+
221+
var loginQuery = $@"
222+
mutation {{
223+
loginAccount(email: ""{email}"", password: ""{password}"") {{
224+
token
225+
account {{
226+
id
227+
}}
228+
}}
229+
}}";
230+
231+
var loginResponse = await _client.PostAsJsonAsync("/graphql", new { query = loginQuery });
232+
var loginContent = await loginResponse.Content.ReadAsStringAsync();
233+
234+
using var doc = JsonDocument.Parse(loginContent);
235+
var token = doc.RootElement.GetProperty("data").GetProperty("loginAccount").GetProperty("token").GetString();
236+
var accountId = doc.RootElement.GetProperty("data").GetProperty("loginAccount").GetProperty("account").GetProperty("id").GetInt32();
237+
238+
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
239+
240+
var getAccountQuery = $@"
241+
query {{
242+
getAccount(id: {accountId}) {{
243+
id
244+
email
245+
currentBalance
246+
}}
247+
}}";
248+
249+
// Act
250+
var response = await _client.PostAsJsonAsync("/graphql", new { query = getAccountQuery });
251+
252+
// Assert
253+
response.EnsureSuccessStatusCode();
254+
var content = await response.Content.ReadAsStringAsync();
255+
content.Should().Contain(email);
256+
content.Should().Contain("currentBalance");
257+
}
258+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using Microsoft.AspNetCore.Hosting;
2+
using Microsoft.AspNetCore.Mvc.Testing;
3+
using Microsoft.AspNetCore.TestHost;
4+
using Microsoft.EntityFrameworkCore;
5+
using Microsoft.Extensions.Configuration;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.DependencyInjection.Extensions;
8+
using PhantomDave.BankTracking.Data.Context;
9+
10+
namespace PhantomDave.BankTracking.IntegrationTests.Helpers;
11+
12+
public class GraphQLTestFactory : WebApplicationFactory<PhantomDave.BankTracking.Api.Program>
13+
{
14+
protected override void ConfigureWebHost(IWebHostBuilder builder)
15+
{
16+
builder.ConfigureAppConfiguration((context, config) =>
17+
{
18+
config.AddInMemoryCollection(new Dictionary<string, string?>
19+
{
20+
["ConnectionStrings:DefaultConnection"] = "Host=localhost;Database=banktrackertest;Username=test;Password=test",
21+
["Jwt:Secret"] = "ThisIsASecretKeyForTestingPurposesOnly123456789",
22+
["Jwt:Issuer"] = "BankTrackerTestIssuer",
23+
["Jwt:Audience"] = "BankTrackerTestAudience",
24+
["Jwt:ExpiryMinutes"] = "60"
25+
});
26+
});
27+
28+
builder.ConfigureTestServices(services =>
29+
{
30+
var descriptor = services.SingleOrDefault(
31+
d => d.ServiceType == typeof(DbContextOptions<BankTrackerDbContext>));
32+
33+
if (descriptor != null)
34+
{
35+
services.Remove(descriptor);
36+
}
37+
38+
services.AddDbContext<BankTrackerDbContext>(options =>
39+
{
40+
options.UseInMemoryDatabase("InMemoryTestDb");
41+
});
42+
});
43+
44+
builder.UseEnvironment("Testing");
45+
}
46+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="coverlet.collector" />
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
13+
<PackageReference Include="xunit" />
14+
<PackageReference Include="xunit.runner.visualstudio" />
15+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
16+
<PackageReference Include="FluentAssertions" />
17+
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<Using Include="Xunit" />
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<ProjectReference Include="..\PhantomDave.BankTracking.Api\PhantomDave.BankTracking.Api.csproj" />
26+
</ItemGroup>
27+
28+
</Project>

0 commit comments

Comments
 (0)