Files
tiku-backend.net/Tiku.IntegrationTests/Api/LearningEndpointTests.cs
2026-07-26 15:18:57 +08:00

275 lines
9.8 KiB
C#

using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Api.Contracts;
using Tiku.Domain.Common;
using Tiku.Domain.Content;
using Tiku.Domain.Identity;
using Tiku.Domain.QuestionBanks;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Auth;
using Tiku.Infrastructure.Persistence;
namespace Tiku.IntegrationTests.Api;
public sealed class LearningEndpointTests
{
[Fact]
public async Task Learning_endpoints_require_current_tenant_member()
{
await using var factory = new ApiTestFactory();
using var client = factory.CreateClient();
using var response = await client.GetAsync("/api/learning/favorites/questions");
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public async Task Submit_answer_records_wrong_question_when_self_judged_wrong()
{
await using var factory = new ApiTestFactory();
var seed = await SeedLearningUserAsync(factory);
var questionId = Guid.NewGuid();
await factory.SeedAsync(new Question
{
Id = questionId,
TenantId = seed.TenantId,
Type = "choice",
Status = QuestionStatus.Published
});
using var client = factory.CreateClient();
await LoginAsync(client, seed);
using var response = await client.PostAsJsonAsync(
"/api/learning/answers",
new SubmitAnswerDto
{
QuestionId = questionId,
SelectedOptions = ["A"],
SelfJudgedCorrect = false
});
using var wrongResponse = await client.GetAsync("/api/learning/wrong-questions");
var answer = await ReadJsonAsync(response);
var wrongItems = await ReadItemsAsync(wrongResponse);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(questionId, answer.RootElement.GetProperty("questionId").GetGuid());
Assert.False(answer.RootElement.GetProperty("isCorrect").GetBoolean());
var wrong = Assert.Single(wrongItems);
Assert.Equal(questionId, wrong.GetProperty("questionId").GetGuid());
Assert.Equal(1, wrong.GetProperty("wrongCount").GetInt32());
}
[Fact]
public async Task Favorite_question_can_be_added_listed_and_removed()
{
await using var factory = new ApiTestFactory();
var seed = await SeedLearningUserAsync(factory);
var questionId = Guid.NewGuid();
await factory.SeedAsync(new Question
{
Id = questionId,
TenantId = seed.TenantId,
Type = "choice",
Status = QuestionStatus.Published
});
using var client = factory.CreateClient();
await LoginAsync(client, seed);
var addResponse = await client.PostAsJsonAsync(
"/api/learning/favorites/questions",
new QuestionActionDto { QuestionId = questionId });
var listResponse = await client.GetAsync("/api/learning/favorites/questions");
var itemsAfterAdd = await ReadItemsAsync(listResponse);
var removeResponse = await client.PostAsJsonAsync(
"/api/learning/favorites/questions",
new QuestionActionDto { QuestionId = questionId, Favorite = false });
var emptyResponse = await client.GetAsync("/api/learning/favorites/questions");
var itemsAfterRemove = await ReadItemsAsync(emptyResponse);
Assert.Equal(HttpStatusCode.OK, addResponse.StatusCode);
Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
Assert.Equal(questionId, Assert.Single(itemsAfterAdd).GetProperty("questionId").GetGuid());
Assert.Equal(HttpStatusCode.OK, removeResponse.StatusCode);
Assert.Empty(itemsAfterRemove);
}
[Fact]
public async Task Word_progress_upserts_and_filters_by_status()
{
await using var factory = new ApiTestFactory();
var seed = await SeedLearningUserAsync(factory);
var wordId = Guid.NewGuid();
await factory.SeedAsync(new VocabularyWord
{
Id = wordId,
TenantId = seed.TenantId,
Word = "composition",
IsActive = true
});
using var client = factory.CreateClient();
await LoginAsync(client, seed);
using var updateResponse = await client.PostAsJsonAsync(
"/api/learning/vocabulary/progress",
new WordProgressDto
{
WordId = wordId,
Status = "learning",
CorrectDelta = 2
});
using var listResponse = await client.GetAsync("/api/learning/vocabulary/progress?status=learning");
var progress = await ReadJsonAsync(updateResponse);
var items = await ReadItemsAsync(listResponse);
Assert.Equal(HttpStatusCode.OK, updateResponse.StatusCode);
Assert.Equal(2, progress.RootElement.GetProperty("correctCount").GetInt32());
var item = Assert.Single(items);
Assert.Equal(wordId, item.GetProperty("wordId").GetGuid());
Assert.Equal("Learning", item.GetProperty("status").GetString());
}
[Fact]
public async Task Favorite_words_can_be_filtered_by_unit()
{
await using var factory = new ApiTestFactory();
var seed = await SeedLearningUserAsync(factory);
var unitId = Guid.NewGuid();
var otherUnitId = Guid.NewGuid();
var wordId = Guid.NewGuid();
var otherWordId = Guid.NewGuid();
await factory.SeedAsync(
new VocabularyUnit
{
Id = unitId,
TenantId = seed.TenantId,
Name = "Unit 1",
IsActive = true
},
new VocabularyUnit
{
Id = otherUnitId,
TenantId = seed.TenantId,
Name = "Unit 2",
IsActive = true
},
new VocabularyWord
{
Id = wordId,
TenantId = seed.TenantId,
UnitId = unitId,
Word = "design",
IsActive = true
},
new VocabularyWord
{
Id = otherWordId,
TenantId = seed.TenantId,
UnitId = otherUnitId,
Word = "sketch",
IsActive = true
});
using var client = factory.CreateClient();
await LoginAsync(client, seed);
await client.PostAsJsonAsync(
"/api/learning/vocabulary/favorites",
new FavoriteWordDto { WordId = wordId, Note = "重点" });
await client.PostAsJsonAsync(
"/api/learning/vocabulary/favorites",
new FavoriteWordDto { WordId = otherWordId });
using var response = await client.GetAsync($"/api/learning/vocabulary/favorites?unitId={unitId}");
var items = await ReadItemsAsync(response);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var item = Assert.Single(items);
Assert.Equal(wordId, item.GetProperty("wordId").GetGuid());
Assert.Equal("重点", item.GetProperty("note").GetString());
}
private static async Task<(Guid TenantId, Guid UserId, string Phone)> SeedLearningUserAsync(
ApiTestFactory factory)
{
var tenantId = Guid.NewGuid();
var userId = Guid.NewGuid();
var phone = "13900000000";
var passwordHash = new PasswordHasher().Hash("passw0rd!");
await factory.SeedAsync(
new Tenant
{
Id = tenantId,
Slug = tenantId.ToString("N"),
Name = "Learning Tenant"
},
new User
{
Id = userId,
Phone = phone,
Name = "Learning User"
},
new TenantMembership
{
TenantId = tenantId,
UserId = userId,
Role = TenantRole.Student,
Status = MembershipStatus.Active
},
new UserIdentity
{
UserId = userId,
Provider = "password",
ProviderSubject = phone,
Phone = phone,
SecretPayload = CreateSecretPayload(passwordHash)
});
return (tenantId, userId, phone);
}
private static async Task LoginAsync(
HttpClient client,
(Guid TenantId, Guid UserId, string Phone) seed)
{
var loginResponse = await client.PostAsJsonAsync(
"/api/auth/login/password",
new PasswordLoginDto
{
TenantId = seed.TenantId,
Phone = seed.Phone,
Password = "passw0rd!"
});
var loginJson = await ReadJsonAsync(loginResponse);
var accessToken = loginJson.RootElement
.GetProperty("tokens")
.GetProperty("accessToken")
.GetString();
client.DefaultRequestHeaders.Authorization = new("Bearer", accessToken);
}
private static async Task<JsonDocument> ReadJsonAsync(HttpResponseMessage response)
{
var stream = await response.Content.ReadAsStreamAsync();
return await JsonDocument.ParseAsync(stream);
}
private static async Task<JsonElement[]> ReadItemsAsync(HttpResponseMessage response)
{
var body = await ReadJsonAsync(response);
return body.RootElement
.GetProperty("items")
.EnumerateArray()
.Select(item => item.Clone())
.ToArray();
}
private static JsonElement CreateSecretPayload(string passwordHash)
{
using var document = JsonDocument.Parse(
$$"""{"passwordHash":{{JsonSerializer.Serialize(passwordHash)}}}""");
return document.RootElement.Clone();
}
}