Files
tiku-backend.net/Tiku.IntegrationTests/ContentV2/ContentReleaseCompilerTests.cs
xiong 3f5957d744
Some checks failed
ci / release-gate (push) Has been cancelled
feat(content): establish v2 learning delivery foundation
2026-08-06 09:52:18 +08:00

237 lines
8.9 KiB
C#

using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Application.Content;
using Tiku.Domain.Content;
using Tiku.Domain.Learning;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;
using Tiku.IntegrationTests.Api;
namespace Tiku.IntegrationTests.ContentV2;
public sealed class ContentReleaseCompilerTests
{
[Fact]
public async Task Publishing_new_revision_creates_new_release_without_mutating_old_delivery()
{
await using var factory = new ApiTestFactory();
var fixture = await SeedAsync(factory);
using var scope = factory.CreateSystemScope("Compile immutable V2 releases");
var compiler = scope.ServiceProvider.GetRequiredService<IContentReleaseCompiler>();
var authoring = scope.ServiceProvider.GetRequiredService<IContentV2AuthoringService>();
var first = await compiler.PublishAsync(new PublishContentReleaseCommand(
fixture.TenantId,
fixture.ActorUserId,
fixture.CurriculumVersionId,
"Release 1"));
var revised = await authoring.CreateRevisionAsync(
new ContentAuthorActor(fixture.TenantId, fixture.ActorUserId),
fixture.QuestionAssetId,
Draft("2 + 2 = ?", 1),
true);
var second = await compiler.PublishAsync(new PublishContentReleaseCommand(
fixture.TenantId,
fixture.ActorUserId,
fixture.CurriculumVersionId,
"Release 2"));
var db = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
var firstQuestion = await db.ContentReleaseQuestions.AsNoTracking()
.SingleAsync(item => item.ContentReleaseId == first.ContentReleaseId);
var secondQuestion = await db.ContentReleaseQuestions.AsNoTracking()
.SingleAsync(item => item.ContentReleaseId == second.ContentReleaseId);
Assert.Equal(1, first.ReleaseNo);
Assert.Equal(2, second.ReleaseNo);
Assert.Equal(fixture.QuestionRevisionId, firstQuestion.QuestionRevisionId);
Assert.Equal(revised.QuestionRevisionId, secondQuestion.QuestionRevisionId);
Assert.NotEqual(firstQuestion.QuestionRevisionId, secondQuestion.QuestionRevisionId);
Assert.Equal(2, await db.LearningOutboxMessages.CountAsync(item =>
item.EventType == "content_release_published"));
Assert.All(await db.ContentReleases.AsNoTracking().ToArrayAsync(), release =>
Assert.Equal(ContentReleaseStatus.Published, release.Status));
}
private static async Task<CompilerFixture> SeedAsync(ApiTestFactory factory)
{
var tenantId = Guid.NewGuid();
var platformOwnerId = Guid.NewGuid();
var actorUserId = Guid.NewGuid();
var businessLineId = Guid.NewGuid();
var profileId = Guid.NewGuid();
var curriculumId = Guid.NewGuid();
var curriculumVersionId = Guid.NewGuid();
var curriculumNodeId = Guid.NewGuid();
var policyId = Guid.NewGuid();
var policyVersionId = Guid.NewGuid();
var questionAssetId = Guid.NewGuid();
var questionRevisionId = Guid.NewGuid();
var placementId = Guid.NewGuid();
var now = DateTimeOffset.UtcNow;
var draft = Draft("1 + 1 = ?", 0);
var fingerprint = QuestionDeliveryFingerprint.Compute(ToFingerprintInput(draft));
await factory.SeedAsync(
new Tenant
{
Id = platformOwnerId,
Slug = $"platform-{platformOwnerId:N}",
Name = "Platform owner",
Mode = TenantMode.PlatformOwned
},
new Tenant
{
Id = tenantId,
Slug = $"tenant-{tenantId:N}",
Name = "V2 compiler tenant"
},
new BusinessLine
{
Id = businessLineId,
Code = $"business-{businessLineId:N}",
Name = "Extensible exam business"
},
new ExamTargetProfile
{
Id = profileId,
BusinessLineId = businessLineId,
Code = $"profile-{profileId:N}",
Name = "Profile"
},
new Curriculum
{
Id = curriculumId,
TenantId = tenantId,
BusinessLineId = businessLineId,
Code = $"curriculum-{curriculumId:N}",
Name = "Curriculum"
},
new AssessmentPolicy
{
Id = policyId,
TenantId = tenantId,
BusinessLineId = businessLineId,
Code = $"policy-{policyId:N}",
Name = "Policy"
},
new QuestionAsset
{
Id = questionAssetId,
TenantId = tenantId,
DeliveryFingerprint = fingerprint,
Status = QuestionAssetStatus.Published,
CreatedBy = actorUserId,
UpdatedBy = actorUserId
});
await factory.SeedAsync(
new ExamTargetProfileVersion
{
ExamTargetProfileId = profileId,
DisplayName = "Profile 2027",
Status = ContentDefinitionStatus.Published,
PublishedAt = now
},
new CurriculumVersion
{
Id = curriculumVersionId,
TenantId = tenantId,
CurriculumId = curriculumId,
Name = "Curriculum 2027",
Status = ContentDefinitionStatus.Published,
PublishedAt = now
},
new AssessmentPolicyVersion
{
Id = policyVersionId,
TenantId = tenantId,
AssessmentPolicyId = policyId,
DefaultScore = 1,
Status = ContentDefinitionStatus.Published,
PublishedAt = now
},
new QuestionRevision
{
Id = questionRevisionId,
TenantId = tenantId,
QuestionAssetId = questionAssetId,
QuestionType = draft.QuestionType,
NormalizedContent = QuestionDeliveryFingerprint.NormalizePrompt(draft.Content),
Content = draft.Content,
Options = draft.Options,
CorrectOptionIndex = draft.CorrectOptionIndex,
DeliveryFingerprint = fingerprint,
CreatedBy = actorUserId
});
await factory.SeedAsync(
new CurriculumNode
{
Id = curriculumNodeId,
TenantId = tenantId,
CurriculumVersionId = curriculumVersionId,
Code = "math",
Name = "Math",
Kind = CurriculumNodeKind.Subject
});
await factory.SeedAsync(
new QuestionPlacement
{
Id = placementId,
TenantId = tenantId,
QuestionAssetOwnerTenantId = tenantId,
QuestionAssetId = questionAssetId,
CurriculumVersionId = curriculumVersionId,
CurriculumNodeId = curriculumNodeId,
AssessmentPolicyVersionId = policyVersionId,
Status = QuestionPlacementStatus.Active,
CreatedBy = actorUserId,
UpdatedBy = actorUserId
});
using (var scope = factory.CreateSystemScope("Set current immutable revision"))
{
var db = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
await db.QuestionAssets.Where(item => item.Id == questionAssetId)
.ExecuteUpdateAsync(setters => setters.SetProperty(item => item.CurrentRevisionId, questionRevisionId));
}
return new CompilerFixture(
tenantId,
actorUserId,
curriculumVersionId,
questionAssetId,
questionRevisionId);
}
private static QuestionRevisionDraft Draft(string content, int correctOptionIndex) => new(
"choice",
null,
2,
content,
JsonSerializer.SerializeToElement(new[] { "2", "4" }),
correctOptionIndex,
JsonSerializer.SerializeToElement(Array.Empty<int>()),
null,
"Explanation",
JsonSerializer.SerializeToElement(Array.Empty<object>()),
null,
null);
private static QuestionDeliveryFingerprintInput ToFingerprintInput(QuestionRevisionDraft draft) => new(
draft.QuestionType,
draft.Content,
draft.Options,
draft.CorrectOptionIndex,
draft.CorrectOptionIndices,
draft.AnswerText,
draft.SubQuestions,
draft.CodeLang,
draft.CodeTemplate);
private sealed record CompilerFixture(
Guid TenantId,
Guid ActorUserId,
Guid CurriculumVersionId,
Guid QuestionAssetId,
Guid QuestionRevisionId);
}