공개: KoTalk 최신 기준선
This commit is contained in:
commit
debf62f76e
572 changed files with 41689 additions and 0 deletions
|
|
@ -0,0 +1,41 @@
|
|||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace PhysOn.Api.IntegrationTests.Infrastructure;
|
||||
|
||||
public sealed class PhysOnApiFactory : WebApplicationFactory<Program>, IAsyncLifetime
|
||||
{
|
||||
private readonly string _databasePath = Path.Combine(Path.GetTempPath(), $"vsmessenger-tests-{Guid.NewGuid():N}.db");
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.UseEnvironment("Testing");
|
||||
var contentRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../../src/PhysOn.Api"));
|
||||
builder.UseContentRoot(contentRoot);
|
||||
builder.ConfigureAppConfiguration((_, configBuilder) =>
|
||||
{
|
||||
var overrides = new Dictionary<string, string?>
|
||||
{
|
||||
["ConnectionStrings:Main"] = $"Data Source={_databasePath}",
|
||||
["Bootstrap:SeedDefaultInviteCodes"] = "true",
|
||||
["Bootstrap:InviteCodes:0"] = "ALPHA-OPEN-2026",
|
||||
["Auth:Jwt:SigningKey"] = "testing-signing-key-should-never-ship-2026-very-secret"
|
||||
};
|
||||
|
||||
configBuilder.AddInMemoryCollection(overrides);
|
||||
});
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
public new async Task DisposeAsync()
|
||||
{
|
||||
await base.DisposeAsync();
|
||||
|
||||
if (File.Exists(_databasePath))
|
||||
{
|
||||
File.Delete(_databasePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<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="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.5.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\PhysOn.Api\PhysOn.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
178
tests/PhysOn.Api.IntegrationTests/VerticalSliceTests.cs
Normal file
178
tests/PhysOn.Api.IntegrationTests/VerticalSliceTests.cs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using PhysOn.Api.IntegrationTests.Infrastructure;
|
||||
using PhysOn.Contracts.Auth;
|
||||
using PhysOn.Contracts.Common;
|
||||
using PhysOn.Contracts.Conversations;
|
||||
|
||||
namespace PhysOn.Api.IntegrationTests;
|
||||
|
||||
public sealed class VerticalSliceTests : IClassFixture<PhysOnApiFactory>
|
||||
{
|
||||
private readonly PhysOnApiFactory _factory;
|
||||
|
||||
public VerticalSliceTests(PhysOnApiFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Register_bootstrap_and_message_flow_work()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var registerResponse = await client.PostAsJsonAsync(
|
||||
"/v1/auth/register/alpha-quick",
|
||||
new RegisterAlphaQuickRequest(
|
||||
"이안",
|
||||
"ALPHA-OPEN-2026",
|
||||
new DeviceRegistrationDto(Guid.NewGuid().ToString(), "windows", "Windows PC", "0.1.0")));
|
||||
|
||||
registerResponse.EnsureSuccessStatusCode();
|
||||
|
||||
var registerPayload = await registerResponse.Content.ReadFromJsonAsync<ApiEnvelope<RegisterAlphaQuickResponse>>();
|
||||
Assert.NotNull(registerPayload);
|
||||
Assert.Equal("이안", registerPayload!.Data.Account.DisplayName);
|
||||
Assert.NotEmpty(registerPayload.Data.Bootstrap.Conversations.Items);
|
||||
|
||||
var accessToken = registerPayload.Data.Tokens.AccessToken;
|
||||
var selfConversationId = registerPayload.Data.Bootstrap.Conversations.Items
|
||||
.Single(x => x.Type == "self")
|
||||
.ConversationId;
|
||||
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
|
||||
var bootstrapPayload = await client.GetFromJsonAsync<ApiEnvelope<BootstrapResponse>>("/v1/bootstrap");
|
||||
Assert.NotNull(bootstrapPayload);
|
||||
Assert.Equal(registerPayload.Data.Account.UserId, bootstrapPayload!.Data.Me.UserId);
|
||||
|
||||
var conversationsPayload = await client.GetFromJsonAsync<ApiEnvelope<ListEnvelope<ConversationSummaryDto>>>("/v1/conversations");
|
||||
Assert.NotNull(conversationsPayload);
|
||||
Assert.NotEmpty(conversationsPayload!.Data.Items);
|
||||
|
||||
var postMessageResponse = await client.PostAsJsonAsync(
|
||||
$"/v1/conversations/{selfConversationId}/messages",
|
||||
new PostTextMessageRequest(Guid.NewGuid(), "첫 메시지"));
|
||||
|
||||
postMessageResponse.EnsureSuccessStatusCode();
|
||||
var messagePayload = await postMessageResponse.Content.ReadFromJsonAsync<ApiEnvelope<MessageItemDto>>();
|
||||
Assert.NotNull(messagePayload);
|
||||
Assert.Equal("첫 메시지", messagePayload!.Data.Text);
|
||||
|
||||
var messagesPayload = await client.GetFromJsonAsync<ApiEnvelope<ListEnvelope<MessageItemDto>>>(
|
||||
$"/v1/conversations/{selfConversationId}/messages");
|
||||
|
||||
Assert.NotNull(messagesPayload);
|
||||
Assert.Contains(messagesPayload!.Data.Items, x => x.Text == "첫 메시지");
|
||||
|
||||
var readCursorResponse = await client.PostAsJsonAsync(
|
||||
$"/v1/conversations/{selfConversationId}/read-cursor",
|
||||
new UpdateReadCursorRequest(messagePayload.Data.ServerSequence));
|
||||
|
||||
readCursorResponse.EnsureSuccessStatusCode();
|
||||
var readCursorPayload = await readCursorResponse.Content.ReadFromJsonAsync<ApiEnvelope<ReadCursorUpdatedDto>>();
|
||||
Assert.NotNull(readCursorPayload);
|
||||
Assert.Equal(messagePayload.Data.ServerSequence, readCursorPayload!.Data.LastReadSequence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Posting_same_client_request_id_is_idempotent()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var registerResponse = await client.PostAsJsonAsync(
|
||||
"/v1/auth/register/alpha-quick",
|
||||
new RegisterAlphaQuickRequest(
|
||||
"테스터",
|
||||
"ALPHA-OPEN-2026",
|
||||
new DeviceRegistrationDto(Guid.NewGuid().ToString(), "windows", "Windows PC", "0.1.0")));
|
||||
|
||||
registerResponse.EnsureSuccessStatusCode();
|
||||
var registerPayload = await registerResponse.Content.ReadFromJsonAsync<ApiEnvelope<RegisterAlphaQuickResponse>>();
|
||||
Assert.NotNull(registerPayload);
|
||||
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", registerPayload!.Data.Tokens.AccessToken);
|
||||
var conversationId = registerPayload.Data.Bootstrap.Conversations.Items.Single(x => x.Type == "self").ConversationId;
|
||||
var request = new PostTextMessageRequest(Guid.NewGuid(), "중복 방지");
|
||||
|
||||
var first = await client.PostAsJsonAsync($"/v1/conversations/{conversationId}/messages", request);
|
||||
var second = await client.PostAsJsonAsync($"/v1/conversations/{conversationId}/messages", request);
|
||||
|
||||
first.EnsureSuccessStatusCode();
|
||||
second.EnsureSuccessStatusCode();
|
||||
|
||||
var firstPayload = await first.Content.ReadFromJsonAsync<ApiEnvelope<MessageItemDto>>();
|
||||
var secondPayload = await second.Content.ReadFromJsonAsync<ApiEnvelope<MessageItemDto>>();
|
||||
|
||||
Assert.NotNull(firstPayload);
|
||||
Assert.NotNull(secondPayload);
|
||||
Assert.Equal(firstPayload!.Data.MessageId, secondPayload!.Data.MessageId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Auth_and_bootstrap_responses_are_marked_no_store()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var registerResponse = await client.PostAsJsonAsync(
|
||||
"/v1/auth/register/alpha-quick",
|
||||
new RegisterAlphaQuickRequest(
|
||||
"보안테스터",
|
||||
"ALPHA-OPEN-2026",
|
||||
new DeviceRegistrationDto(Guid.NewGuid().ToString(), "windows", "Windows PC", "0.1.0")));
|
||||
|
||||
registerResponse.EnsureSuccessStatusCode();
|
||||
Assert.True(registerResponse.Headers.CacheControl?.NoStore ?? false);
|
||||
|
||||
var registerPayload = await registerResponse.Content.ReadFromJsonAsync<ApiEnvelope<RegisterAlphaQuickResponse>>();
|
||||
Assert.NotNull(registerPayload);
|
||||
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", registerPayload!.Data.Tokens.AccessToken);
|
||||
|
||||
var bootstrapResponse = await client.GetAsync("/v1/bootstrap");
|
||||
bootstrapResponse.EnsureSuccessStatusCode();
|
||||
Assert.True(bootstrapResponse.Headers.CacheControl?.NoStore ?? false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Protected_reads_are_marked_no_store()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var registerResponse = await client.PostAsJsonAsync(
|
||||
"/v1/auth/register/alpha-quick",
|
||||
new RegisterAlphaQuickRequest(
|
||||
"캐시테스터",
|
||||
"ALPHA-OPEN-2026",
|
||||
new DeviceRegistrationDto(Guid.NewGuid().ToString(), "windows", "Windows PC", "0.1.0")));
|
||||
|
||||
registerResponse.EnsureSuccessStatusCode();
|
||||
var registerPayload = await registerResponse.Content.ReadFromJsonAsync<ApiEnvelope<RegisterAlphaQuickResponse>>();
|
||||
Assert.NotNull(registerPayload);
|
||||
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", registerPayload!.Data.Tokens.AccessToken);
|
||||
var conversationId = registerPayload.Data.Bootstrap.Conversations.Items.Single(x => x.Type == "self").ConversationId;
|
||||
|
||||
var meResponse = await client.GetAsync("/v1/me");
|
||||
meResponse.EnsureSuccessStatusCode();
|
||||
Assert.True(meResponse.Headers.CacheControl?.NoStore ?? false);
|
||||
|
||||
var conversationsResponse = await client.GetAsync("/v1/conversations");
|
||||
conversationsResponse.EnsureSuccessStatusCode();
|
||||
Assert.True(conversationsResponse.Headers.CacheControl?.NoStore ?? false);
|
||||
|
||||
var messagesResponse = await client.GetAsync($"/v1/conversations/{conversationId}/messages");
|
||||
messagesResponse.EnsureSuccessStatusCode();
|
||||
Assert.True(messagesResponse.Headers.CacheControl?.NoStore ?? false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Refresh_without_token_returns_unauthorized()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.PostAsJsonAsync("/v1/auth/token/refresh", new RefreshTokenRequest(string.Empty));
|
||||
|
||||
Assert.Equal(System.Net.HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue