237 lines
8.9 KiB
C#
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);
|
|
}
|