공개: KoTalk 최신 기준선

This commit is contained in:
Ian 2026-04-16 09:24:26 +09:00
commit debf62f76e
572 changed files with 41689 additions and 0 deletions

View file

@ -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);
}
}
}

View file

@ -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>

View 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);
}
}