forked from gongxuegit/tiku-backend.net
feat: enforce tenant isolation and shared question bank
This commit is contained in:
@@ -6,6 +6,7 @@ using Tiku.Api.Contracts;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Learning;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Auth;
|
||||
@@ -32,13 +33,7 @@ public sealed class LearningEndpointTests
|
||||
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
|
||||
});
|
||||
var sessionQuestionId = await SeedAnswerableQuestionAsync(factory, seed, questionId);
|
||||
using var client = factory.CreateClient();
|
||||
await LoginAsync(client, seed);
|
||||
|
||||
@@ -46,7 +41,7 @@ public sealed class LearningEndpointTests
|
||||
"/api/learning/answers",
|
||||
new SubmitAnswerDto
|
||||
{
|
||||
QuestionId = questionId,
|
||||
SessionQuestionId = sessionQuestionId,
|
||||
SelectedOptions = ["A"],
|
||||
SelfJudgedCorrect = false
|
||||
});
|
||||
@@ -55,10 +50,10 @@ public sealed class LearningEndpointTests
|
||||
var wrongItems = await ReadItemsAsync(wrongResponse);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(questionId, answer.RootElement.GetProperty("questionId").GetGuid());
|
||||
Assert.Equal(sessionQuestionId, answer.RootElement.GetProperty("sessionQuestionId").GetGuid());
|
||||
Assert.False(answer.RootElement.GetProperty("isCorrect").GetBoolean());
|
||||
var wrong = Assert.Single(wrongItems);
|
||||
Assert.Equal(questionId, wrong.GetProperty("questionId").GetGuid());
|
||||
Assert.Equal(questionId, wrong.GetProperty("locator").GetProperty("questionId").GetGuid());
|
||||
Assert.Equal(1, wrong.GetProperty("wrongCount").GetInt32());
|
||||
}
|
||||
|
||||
@@ -70,14 +65,9 @@ public sealed class LearningEndpointTests
|
||||
var collectionId = Guid.NewGuid();
|
||||
var firstQuestionId = Guid.NewGuid();
|
||||
var secondQuestionId = Guid.NewGuid();
|
||||
await factory.SeedAsync(
|
||||
new QuestionCollection
|
||||
{
|
||||
Id = collectionId,
|
||||
TenantId = seed.TenantId,
|
||||
Name = "基础练习",
|
||||
Status = ContentStatus.Active
|
||||
},
|
||||
var firstVersionId = Guid.NewGuid();
|
||||
var secondVersionId = Guid.NewGuid();
|
||||
await factory.SeedQuestionWithVersionAsync(
|
||||
new Question
|
||||
{
|
||||
Id = firstQuestionId,
|
||||
@@ -85,6 +75,15 @@ public sealed class LearningEndpointTests
|
||||
Type = "choice",
|
||||
Status = QuestionStatus.Published
|
||||
},
|
||||
new QuestionVersion
|
||||
{
|
||||
Id = firstVersionId,
|
||||
TenantId = seed.TenantId,
|
||||
QuestionId = firstQuestionId,
|
||||
VersionNo = 1,
|
||||
Content = "first"
|
||||
});
|
||||
await factory.SeedQuestionWithVersionAsync(
|
||||
new Question
|
||||
{
|
||||
Id = secondQuestionId,
|
||||
@@ -92,11 +91,47 @@ public sealed class LearningEndpointTests
|
||||
Type = "choice",
|
||||
Status = QuestionStatus.Published
|
||||
},
|
||||
new QuestionVersion
|
||||
{
|
||||
Id = secondVersionId,
|
||||
TenantId = seed.TenantId,
|
||||
QuestionId = secondQuestionId,
|
||||
VersionNo = 1,
|
||||
Content = "second"
|
||||
});
|
||||
var firstReferenceId = Guid.NewGuid();
|
||||
var secondReferenceId = Guid.NewGuid();
|
||||
await factory.SeedAsync(
|
||||
new TenantQuestionReference
|
||||
{
|
||||
Id = firstReferenceId,
|
||||
TenantId = seed.TenantId,
|
||||
QuestionOwnerTenantId = seed.TenantId,
|
||||
QuestionId = firstQuestionId,
|
||||
Source = QuestionSource.Tenant
|
||||
},
|
||||
new TenantQuestionReference
|
||||
{
|
||||
Id = secondReferenceId,
|
||||
TenantId = seed.TenantId,
|
||||
QuestionOwnerTenantId = seed.TenantId,
|
||||
QuestionId = secondQuestionId,
|
||||
Source = QuestionSource.Tenant
|
||||
},
|
||||
new QuestionCollection
|
||||
{
|
||||
Id = collectionId,
|
||||
TenantId = seed.TenantId,
|
||||
Name = "基础练习",
|
||||
Status = ContentStatus.Active
|
||||
},
|
||||
new QuestionCollectionItem
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = seed.TenantId,
|
||||
CollectionId = collectionId,
|
||||
QuestionReferenceId = firstReferenceId,
|
||||
QuestionOwnerTenantId = seed.TenantId,
|
||||
QuestionId = firstQuestionId,
|
||||
SortOrder = 1
|
||||
},
|
||||
@@ -105,6 +140,8 @@ public sealed class LearningEndpointTests
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = seed.TenantId,
|
||||
CollectionId = collectionId,
|
||||
QuestionReferenceId = secondReferenceId,
|
||||
QuestionOwnerTenantId = seed.TenantId,
|
||||
QuestionId = secondQuestionId,
|
||||
SortOrder = 2
|
||||
});
|
||||
@@ -124,13 +161,17 @@ public sealed class LearningEndpointTests
|
||||
var practiceSessionId = created.RootElement.GetProperty("id").GetGuid();
|
||||
var detailResponse = await client.GetAsync($"/api/learning/practice-sessions/detail?practiceSessionId={practiceSessionId}");
|
||||
var detail = await ReadJsonAsync(detailResponse);
|
||||
var detailQuestions = detail.RootElement.GetProperty("questions").EnumerateArray().ToArray();
|
||||
var firstSessionQuestionId = detailQuestions.Single(item =>
|
||||
item.GetProperty("questionId").GetGuid() == firstQuestionId).GetProperty("sessionQuestionId").GetGuid();
|
||||
var secondSessionQuestionId = detailQuestions.Single(item =>
|
||||
item.GetProperty("questionId").GetGuid() == secondQuestionId).GetProperty("sessionQuestionId").GetGuid();
|
||||
|
||||
await client.PostAsJsonAsync(
|
||||
"/api/learning/answers",
|
||||
new SubmitAnswerDto
|
||||
{
|
||||
QuestionId = firstQuestionId,
|
||||
PracticeSessionId = practiceSessionId,
|
||||
SessionQuestionId = firstSessionQuestionId,
|
||||
SelectedOptions = ["A"],
|
||||
SelfJudgedCorrect = true
|
||||
});
|
||||
@@ -138,8 +179,7 @@ public sealed class LearningEndpointTests
|
||||
"/api/learning/answers",
|
||||
new SubmitAnswerDto
|
||||
{
|
||||
QuestionId = secondQuestionId,
|
||||
PracticeSessionId = practiceSessionId,
|
||||
SessionQuestionId = secondSessionQuestionId,
|
||||
SelectedOptions = ["B"],
|
||||
SelfJudgedCorrect = false
|
||||
});
|
||||
@@ -199,7 +239,9 @@ public sealed class LearningEndpointTests
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, addResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
|
||||
Assert.Equal(questionId, Assert.Single(itemsAfterAdd).GetProperty("questionId").GetGuid());
|
||||
Assert.Equal(
|
||||
questionId,
|
||||
Assert.Single(itemsAfterAdd).GetProperty("locator").GetProperty("questionId").GetGuid());
|
||||
Assert.Equal(HttpStatusCode.OK, removeResponse.StatusCode);
|
||||
Assert.Empty(itemsAfterRemove);
|
||||
}
|
||||
@@ -304,14 +346,8 @@ public sealed class LearningEndpointTests
|
||||
var seed = await SeedLearningUserAsync(factory);
|
||||
var questionId = Guid.NewGuid();
|
||||
var wordId = Guid.NewGuid();
|
||||
var sessionQuestionId = await SeedAnswerableQuestionAsync(factory, seed, questionId);
|
||||
await factory.SeedAsync(
|
||||
new Question
|
||||
{
|
||||
Id = questionId,
|
||||
TenantId = seed.TenantId,
|
||||
Type = "choice",
|
||||
Status = QuestionStatus.Published
|
||||
},
|
||||
new VocabularyWord
|
||||
{
|
||||
Id = wordId,
|
||||
@@ -326,7 +362,7 @@ public sealed class LearningEndpointTests
|
||||
"/api/learning/answers",
|
||||
new SubmitAnswerDto
|
||||
{
|
||||
QuestionId = questionId,
|
||||
SessionQuestionId = sessionQuestionId,
|
||||
SelectedOptions = ["A"],
|
||||
SelfJudgedCorrect = false
|
||||
});
|
||||
@@ -409,6 +445,64 @@ public sealed class LearningEndpointTests
|
||||
return (tenantId, userId, phone);
|
||||
}
|
||||
|
||||
private static async Task<Guid> SeedAnswerableQuestionAsync(
|
||||
ApiTestFactory factory,
|
||||
(Guid TenantId, Guid UserId, string Phone) seed,
|
||||
Guid questionId)
|
||||
{
|
||||
var versionId = Guid.NewGuid();
|
||||
await factory.SeedQuestionWithVersionAsync(
|
||||
new Question
|
||||
{
|
||||
Id = questionId,
|
||||
TenantId = seed.TenantId,
|
||||
Type = "choice",
|
||||
Status = QuestionStatus.Published
|
||||
},
|
||||
new QuestionVersion
|
||||
{
|
||||
Id = versionId,
|
||||
TenantId = seed.TenantId,
|
||||
QuestionId = questionId,
|
||||
VersionNo = 1,
|
||||
Content = "answerable question"
|
||||
});
|
||||
|
||||
var referenceId = Guid.NewGuid();
|
||||
var sessionId = Guid.NewGuid();
|
||||
var sessionQuestionId = Guid.NewGuid();
|
||||
await factory.SeedAsync(
|
||||
new TenantQuestionReference
|
||||
{
|
||||
Id = referenceId,
|
||||
TenantId = seed.TenantId,
|
||||
QuestionOwnerTenantId = seed.TenantId,
|
||||
QuestionId = questionId,
|
||||
Source = QuestionSource.Tenant
|
||||
},
|
||||
new PracticeSession
|
||||
{
|
||||
Id = sessionId,
|
||||
TenantId = seed.TenantId,
|
||||
UserId = seed.UserId,
|
||||
Mode = "single",
|
||||
QuestionCount = 1,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(30)
|
||||
},
|
||||
new PracticeSessionQuestion
|
||||
{
|
||||
Id = sessionQuestionId,
|
||||
TenantId = seed.TenantId,
|
||||
PracticeSessionId = sessionId,
|
||||
QuestionReferenceId = referenceId,
|
||||
QuestionOwnerTenantId = seed.TenantId,
|
||||
QuestionId = questionId,
|
||||
QuestionVersionId = versionId,
|
||||
Position = 0
|
||||
});
|
||||
return sessionQuestionId;
|
||||
}
|
||||
|
||||
private static async Task LoginAsync(
|
||||
HttpClient client,
|
||||
(Guid TenantId, Guid UserId, string Phone) seed)
|
||||
@@ -417,7 +511,7 @@ public sealed class LearningEndpointTests
|
||||
"/api/auth/login/password",
|
||||
new PasswordLoginDto
|
||||
{
|
||||
TenantId = seed.TenantId,
|
||||
TenantCode = seed.TenantId.ToString("N"),
|
||||
Phone = seed.Phone,
|
||||
Password = "passw0rd!"
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user