forked from xiongyuxing/tiku-backend.net
275 lines
9.8 KiB
C#
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();
|
|
}
|
|
}
|