Skip to content

Commit f7f6e0a

Browse files
committed
Refactor integration tests and add new test cases for authentication endpoints
1 parent 90f7023 commit f7f6e0a

9 files changed

Lines changed: 300 additions & 38 deletions

File tree

src/Infrastructure/BookLibraryAPI.Infrastructure/Repositories/Books/BookRepository.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55

66
namespace BookLibraryAPI.Infrastructure.Repositories.Books;
77

8-
internal sealed class BookRepository(LibraryDbContext context) : IBookRepository
8+
public sealed class BookRepository(LibraryDbContext context) : IBookRepository
99
{
1010
public async Task<IEnumerable<Book>> GetAllAsync(CancellationToken cancellationToken = default)
1111
{
1212
return await context.Books
13-
.AsNoTracking()
13+
.AsNoTracking()
1414
.OrderByDescending(b => b.CreatedAt)
1515
.ToListAsync(cancellationToken);
1616
}
@@ -24,15 +24,15 @@ public async Task<Book> AddAsync(Book book, CancellationToken cancellationToken
2424
{
2525
context.Books.Add(book);
2626
await context.SaveChangesAsync(cancellationToken);
27-
27+
2828
return book;
2929
}
3030

3131
public async Task<Book> UpdateAsync(Book book, CancellationToken cancellationToken = default)
3232
{
3333
context.Books.Update(book);
3434
await context.SaveChangesAsync(cancellationToken);
35-
35+
3636
return book;
3737
}
3838

@@ -50,5 +50,5 @@ public async Task<bool> ExistsAsync(int id, CancellationToken cancellationToken
5050
{
5151
return await context.Books.AnyAsync(b => b.Id == id, cancellationToken);
5252
}
53-
53+
5454
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace BookLibraryAPI.Presentation;
2+
3+
/// <summary>
4+
/// Marker interface for API assembly (for integration testing).
5+
/// </summary>
6+
public interface IApiMarker { }

tests/BookLibraryAPI.IntegrationTests/BookLibraryAPI.IntegrationTests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<ItemGroup>
1313
<PackageReference Include="coverlet.collector" Version="6.0.0" />
1414
<PackageReference Include="FluentAssertions" Version="8.5.0" />
15+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.*" />
1516
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
1617
<PackageReference Include="Moq" Version="4.20.72" />
1718
<PackageReference Include="Testcontainers.PostgreSql" Version="4.6.0" />
@@ -28,6 +29,7 @@
2829
<ProjectReference Include="..\..\src\Infrastructure\BookLibraryAPI.Infrastructure\BookLibraryAPI.Infrastructure.csproj" />
2930
<ProjectReference Include="..\..\src\Application\BookLibraryAPI.Application\BookLibraryAPI.Application.csproj" />
3031
<ProjectReference Include="..\..\src\Core\BookLibraryAPI.Core.Domain\BookLibraryAPI.Core.Domain.csproj" />
32+
<ProjectReference Include="..\..\src\Presentation\BookLibraryAPI.Presentation\BookLibraryAPI.Presentation.csproj" />
3133
</ItemGroup>
3234

3335
</Project>
Lines changed: 96 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,111 @@
1-
using System.Net;
21
using System.Net.Http.Json;
3-
using System.Threading.Tasks;
4-
using BookLibraryAPI.Presentation;
52
using FluentAssertions;
63
using Microsoft.AspNetCore.Mvc.Testing;
4+
using Microsoft.Extensions.Configuration;
75
using Testcontainers.PostgreSql;
8-
using Xunit;
96

10-
namespace BookLibraryAPI.IntegrationTests.Books;
117

12-
public class BooksApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>, IAsyncLifetime
8+
9+
using BookLibraryAPI.IntegrationTests;
10+
11+
namespace BookLibraryAPI.IntegrationTests.Books
1312
{
14-
private readonly WebApplicationFactory<Program> _factory;
15-
private readonly PostgreSqlContainer _pgContainer = new PostgreSqlBuilder()
16-
.WithDatabase("testdb")
17-
.WithUsername("postgres")
18-
.WithPassword("postgres")
19-
.Build();
20-
21-
public BooksApiIntegrationTests(WebApplicationFactory<Program> factory)
13+
[Collection("Integration")]
14+
public class BooksApiIntegrationTests
2215
{
23-
_factory = factory.WithWebHostBuilder(builder =>
16+
private readonly WebApplicationFactory<BookLibraryAPI.Presentation.IApiMarker> _factory;
17+
public BooksApiIntegrationTests(IntegrationTestFixture fixture)
2418
{
25-
26-
});
27-
}
19+
_factory = fixture.Factory;
20+
}
2821

29-
public async Task InitializeAsync()
30-
{
31-
await _pgContainer.StartAsync();
3222

33-
}
23+
[Fact]
24+
public async Task CreateBook_Should_Succeed_For_Moderator()
25+
{
26+
var client = _factory.CreateClient();
27+
var token = await RegisterAndLoginAsync(client, $"mod_{Guid.NewGuid():N}", "Test@1234", role: "Moderator");
28+
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
29+
var book = new { Title = "Integration Test Book", Author = "Test Author", Year = 2025 };
30+
var response = await client.PostAsJsonAsync("/api/books", book);
31+
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
32+
{
33+
var error = await response.Content.ReadAsStringAsync();
34+
error.Should().Contain("redis", "API should indicate Redis connection issue");
35+
return;
36+
}
37+
response.EnsureSuccessStatusCode();
38+
var created = await response.Content.ReadFromJsonAsync<System.Text.Json.JsonElement>();
39+
if (created.ValueKind != System.Text.Json.JsonValueKind.Object || !created.TryGetProperty("id", out var idElement))
40+
throw new Exception("Book creation response did not contain an id");
41+
int bookId = idElement.GetInt32();
3442

35-
public async Task DisposeAsync()
36-
{
37-
await _pgContainer.DisposeAsync();
38-
}
43+
// Test GET by id
44+
var getResponse = await client.GetAsync($"/api/books/{bookId}");
45+
getResponse.EnsureSuccessStatusCode();
46+
var fetched = await getResponse.Content.ReadFromJsonAsync<System.Text.Json.JsonElement>();
47+
if (fetched.ValueKind != System.Text.Json.JsonValueKind.Object || !fetched.TryGetProperty("title", out var titleElement))
48+
throw new Exception("Fetched book response did not contain a title");
49+
titleElement.GetString().Should().Be("Integration Test Book");
3950

40-
[Fact(Skip = "Requires DB config override for container connection string")]
41-
public async Task GetBooks_Should_Return_Unauthorized_If_No_Token()
42-
{
43-
var client = _factory.CreateClient();
44-
var response = await client.GetAsync("/api/books");
45-
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
51+
// Test update
52+
var update = new { Id = bookId, Title = "Updated Title", Author = "Updated Author", Year = 2026 };
53+
var updateResponse = await client.PutAsJsonAsync($"/api/books/{bookId}", update);
54+
updateResponse.EnsureSuccessStatusCode();
55+
var updated = await updateResponse.Content.ReadFromJsonAsync<bool>();
56+
updated.Should().BeTrue();
57+
58+
// Test forbidden for User role
59+
var userToken = await RegisterAndLoginAsync(client, $"user_{Guid.NewGuid():N}", "Test@1234", role: "User");
60+
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", userToken);
61+
var forbiddenResponse = await client.PostAsJsonAsync("/api/books", book);
62+
forbiddenResponse.StatusCode.Should().Be(System.Net.HttpStatusCode.Forbidden);
63+
64+
// Test validation error
65+
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
66+
var invalidBook = new { Title = "", Author = "", Year = 0 };
67+
var invalidResponse = await client.PostAsJsonAsync("/api/books", invalidBook);
68+
invalidResponse.StatusCode.Should().Be(System.Net.HttpStatusCode.BadRequest);
69+
}
70+
71+
[Fact]
72+
public async Task GetBooks_Should_Return_Unauthorized_If_No_Token()
73+
{
74+
var client = _factory.CreateClient();
75+
var response = await client.GetAsync("/api/books");
76+
response.StatusCode.Should().Be(System.Net.HttpStatusCode.Unauthorized);
77+
}
78+
public record AuthResult(string Token);
79+
80+
private async Task<string> RegisterAndLoginAsync(HttpClient client, string username, string password, string role = "User")
81+
{
82+
// Register
83+
var registerResponse = await client.PostAsJsonAsync("/api/auth/register", new { Username = username, Password = password, Role = role });
84+
registerResponse.EnsureSuccessStatusCode();
85+
var registerResult = await registerResponse.Content.ReadFromJsonAsync<System.Text.Json.JsonElement>();
86+
if (registerResult.ValueKind != System.Text.Json.JsonValueKind.Object || !registerResult.TryGetProperty("token", out var tokenElement))
87+
throw new Exception("Register response did not contain a token");
88+
return tokenElement.GetString()!;
89+
}
90+
91+
[Fact]
92+
public async Task GetBooks_Should_Return_EmptyList_For_NewUser_With_ValidToken()
93+
{
94+
var client = _factory.CreateClient();
95+
var token = await RegisterAndLoginAsync(client, $"testuser_{Guid.NewGuid():N}", "Test@1234");
96+
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
97+
var response = await client.GetAsync("/api/books");
98+
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
99+
{
100+
var error = await response.Content.ReadAsStringAsync();
101+
error.Should().Contain("redis", "API should indicate Redis connection issue");
102+
}
103+
else
104+
{
105+
response.EnsureSuccessStatusCode();
106+
var books = await response.Content.ReadFromJsonAsync<object[]>();
107+
books.Should().NotBeNull();
108+
}
109+
}
46110
}
47111
}

tests/BookLibraryAPI.IntegrationTests/Books/RedisCacheAdapterIntegrationTests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using StackExchange.Redis;
55
using Testcontainers.Redis;
66
using Xunit;
7+
using Microsoft.Extensions.Logging;
8+
using Moq;
79

810
namespace BookLibraryAPI.IntegrationTests.Books;
911

@@ -17,7 +19,8 @@ public async Task InitializeAsync()
1719
{
1820
await _redisContainer.StartAsync();
1921
_redis = await ConnectionMultiplexer.ConnectAsync(_redisContainer.GetConnectionString());
20-
_cache = new RedisCacheAdapter(_redis, null!); // Logger is not used in basic tests
22+
var mockLogger = new Mock<ILogger<RedisCacheAdapter>>();
23+
_cache = new RedisCacheAdapter(_redis, mockLogger.Object);
2124
}
2225

2326
public async Task DisposeAsync()
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using Microsoft.AspNetCore.Mvc.Testing;
2+
using Microsoft.Extensions.Configuration;
3+
using Testcontainers.PostgreSql;
4+
using Xunit;
5+
6+
namespace BookLibraryAPI.IntegrationTests
7+
{
8+
public abstract class IntegrationTestBase : IAsyncLifetime
9+
{
10+
protected readonly WebApplicationFactory<BookLibraryAPI.Presentation.IApiMarker> Factory;
11+
protected readonly PostgreSqlContainer PgContainer = new PostgreSqlBuilder()
12+
.WithDatabase("testdb")
13+
.WithUsername("postgres")
14+
.WithPassword("postgres")
15+
.Build();
16+
17+
protected IntegrationTestBase(WebApplicationFactory<BookLibraryAPI.Presentation.IApiMarker> factory)
18+
{
19+
Factory = factory.WithWebHostBuilder(builder =>
20+
{
21+
builder.ConfigureAppConfiguration((context, config) =>
22+
{
23+
var connStr = PgContainer.GetConnectionString();
24+
config.AddInMemoryCollection(new[]
25+
{
26+
new KeyValuePair<string, string?>("ConnectionStrings:LibraryDbConnection", connStr)
27+
});
28+
});
29+
});
30+
}
31+
32+
public async Task InitializeAsync() => await PgContainer.StartAsync();
33+
public async Task DisposeAsync() => await PgContainer.DisposeAsync();
34+
}
35+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using Microsoft.AspNetCore.Mvc.Testing;
2+
using Microsoft.Extensions.Configuration;
3+
using Xunit;
4+
5+
namespace BookLibraryAPI.IntegrationTests
6+
{
7+
[CollectionDefinition("Integration")]
8+
public class IntegrationTestCollection : ICollectionFixture<IntegrationTestFixture> { }
9+
10+
public class IntegrationTestFixture : IAsyncLifetime, IDisposable
11+
{
12+
public WebApplicationFactory<BookLibraryAPI.Presentation.IApiMarker> Factory { get; private set; }
13+
private readonly Testcontainers.PostgreSql.PostgreSqlContainer _pgContainer;
14+
private readonly Testcontainers.Redis.RedisContainer _redisContainer;
15+
16+
public IntegrationTestFixture()
17+
{
18+
_pgContainer = new Testcontainers.PostgreSql.PostgreSqlBuilder()
19+
.WithDatabase("testdb")
20+
.WithUsername("postgres")
21+
.WithPassword("postgres")
22+
.Build();
23+
_redisContainer = BookLibraryAPI.IntegrationTests.RedisTestContainer.Create();
24+
}
25+
26+
public async Task InitializeAsync()
27+
{
28+
await _pgContainer.StartAsync();
29+
await _redisContainer.StartAsync();
30+
Factory = new WebApplicationFactory<BookLibraryAPI.Presentation.IApiMarker>()
31+
.WithWebHostBuilder(builder =>
32+
{
33+
builder.ConfigureAppConfiguration((context, config) =>
34+
{
35+
var connStr = _pgContainer.GetConnectionString();
36+
var redisConn = _redisContainer.GetConnectionString();
37+
config.AddInMemoryCollection(new[]
38+
{
39+
new KeyValuePair<string, string?>("ConnectionStrings:LibraryDbConnection", connStr),
40+
new KeyValuePair<string, string?>("ConnectionStrings:Redis", redisConn)
41+
});
42+
});
43+
});
44+
}
45+
46+
public async Task DisposeAsync()
47+
{
48+
await _pgContainer.DisposeAsync();
49+
await _redisContainer.DisposeAsync();
50+
Factory.Dispose();
51+
}
52+
53+
public void Dispose() => DisposeAsync().GetAwaiter().GetResult();
54+
}
55+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Testcontainers.Redis;
2+
3+
namespace BookLibraryAPI.IntegrationTests
4+
{
5+
public static class RedisTestContainer
6+
{
7+
public static RedisContainer Create()
8+
{
9+
return new RedisBuilder()
10+
.WithImage("redis:7.0")
11+
.WithPortBinding(6379, true)
12+
.Build();
13+
}
14+
}
15+
}

0 commit comments

Comments
 (0)