165 lines
6.2 KiB
C#
165 lines
6.2 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Tiku.Api.Contracts;
|
|
using Tiku.Application.Assets;
|
|
using Tiku.Application.Catalog;
|
|
using Tiku.Domain.Common;
|
|
using Tiku.Domain.Content;
|
|
using Tiku.Domain.Identity;
|
|
using Tiku.Domain.QuestionBanks;
|
|
using Tiku.Domain.Tenancy;
|
|
using Tiku.Infrastructure.Persistence;
|
|
|
|
namespace Tiku.IntegrationTests.Api;
|
|
|
|
public sealed class VideoEndpointTests
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
|
{
|
|
Converters = { new JsonStringEnumConverter() }
|
|
};
|
|
|
|
[Fact]
|
|
public async Task Student_can_search_play_report_progress_and_query_question_videos()
|
|
{
|
|
await using var factory = new ApiTestFactory();
|
|
var seed = await SeedStudentAsync(factory);
|
|
var questionId = Guid.NewGuid();
|
|
var videoId = Guid.NewGuid();
|
|
await factory.SeedAsync(
|
|
new Question
|
|
{ Id = questionId, TenantId = seed.TenantId, Type = "choice", Status = QuestionStatus.Published },
|
|
new VideoExplanation
|
|
{
|
|
Id = videoId,
|
|
TenantId = seed.TenantId,
|
|
Title = "透视解析",
|
|
Description = "素描透视",
|
|
VideoUrl = "https://cdn.example.test/video.mp4",
|
|
DurationSeconds = 120,
|
|
IsActive = true
|
|
},
|
|
new QuestionVideo
|
|
{
|
|
TenantId = seed.TenantId,
|
|
QuestionId = questionId,
|
|
VideoId = videoId
|
|
});
|
|
using var client = factory.CreateClient();
|
|
await LoginAsync(client, seed);
|
|
|
|
var search =
|
|
await client.GetFromJsonAsync<CatalogList<VideoExplanationCatalogItem>>(
|
|
"/api/student/videos/search?keyword=透视", JsonOptions);
|
|
var questionVideos =
|
|
await client.GetFromJsonAsync<CatalogList<QuestionVideoCatalogItem>>(
|
|
$"/api/student/questions/videos?questionId={questionId}", JsonOptions);
|
|
var batch = await (await client.PostAsJsonAsync(
|
|
"/api/student/questions/videos/batch",
|
|
new QuestionVideoQueryDto { QuestionIds = [questionId] }))
|
|
.Content
|
|
.ReadFromJsonAsync<CatalogList<QuestionVideoCatalogItem>>(JsonOptions);
|
|
var playResponse = await client.PostAsJsonAsync(
|
|
"/api/student/videos/play",
|
|
new VideoPlayDto { VideoId = videoId, QuestionId = questionId });
|
|
var play = await playResponse.Content.ReadFromJsonAsync<VideoPlaybackItem>(JsonOptions);
|
|
var progressResponse = await client.PostAsJsonAsync(
|
|
"/api/student/videos/progress",
|
|
new VideoProgressDto
|
|
{
|
|
VideoId = videoId,
|
|
QuestionId = questionId,
|
|
PositionSeconds = 120,
|
|
DurationSeconds = 120,
|
|
WatchedSeconds = 120
|
|
});
|
|
var progress = await progressResponse.Content.ReadFromJsonAsync<VideoProgressItem>(JsonOptions);
|
|
|
|
Assert.Single(search!.Items);
|
|
Assert.Single(questionVideos!.Items);
|
|
Assert.Single(batch!.Items);
|
|
Assert.Equal(HttpStatusCode.OK, playResponse.StatusCode);
|
|
Assert.Equal("https://cdn.example.test/video.mp4", play!.PlayUrl);
|
|
Assert.Equal(HttpStatusCode.OK, progressResponse.StatusCode);
|
|
Assert.True(progress!.IsCompleted);
|
|
|
|
using var scope = factory.CreateSystemScope();
|
|
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
|
|
Assert.Single(dbContext.VideoPlaybackProgress.Where(item =>
|
|
item.TenantId == seed.TenantId &&
|
|
item.UserId == seed.UserId &&
|
|
item.VideoId == videoId &&
|
|
item.QuestionId == questionId));
|
|
Assert.Single(dbContext.ContentAssetAccessEvents.Where(item =>
|
|
item.TenantId == seed.TenantId &&
|
|
item.UserId == seed.UserId &&
|
|
item.AssetType == "Video"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Student_cannot_play_another_tenant_video()
|
|
{
|
|
await using var factory = new ApiTestFactory();
|
|
var tenantA = await SeedStudentAsync(factory, "13900001001");
|
|
var tenantB = await SeedStudentAsync(factory, "13900001002");
|
|
var videoId = Guid.NewGuid();
|
|
await factory.SeedAsync(new VideoExplanation
|
|
{
|
|
Id = videoId,
|
|
TenantId = tenantA.TenantId,
|
|
Title = "A tenant video",
|
|
VideoUrl = "https://cdn.example.test/a.mp4",
|
|
IsActive = true
|
|
});
|
|
using var client = factory.CreateClient();
|
|
await LoginAsync(client, tenantB);
|
|
|
|
var response = await client.PostAsJsonAsync(
|
|
"/api/student/videos/play",
|
|
new VideoPlayDto { VideoId = videoId });
|
|
|
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
|
}
|
|
|
|
private static async Task<(Guid TenantId, Guid UserId, string Phone)> SeedStudentAsync(
|
|
ApiTestFactory factory,
|
|
string? phone = null)
|
|
{
|
|
var tenantId = Guid.NewGuid();
|
|
var userId = Guid.NewGuid();
|
|
var resolvedPhone = phone ?? $"135{Random.Shared.Next(10000000, 99999999)}";
|
|
await factory.SeedAsync(
|
|
new Tenant
|
|
{
|
|
Id = tenantId,
|
|
Slug = tenantId.ToString("N"),
|
|
Name = "Video Tenant",
|
|
Status = TenantStatus.Active,
|
|
Metadata = JsonDefaults.Object()
|
|
},
|
|
new User
|
|
{
|
|
Id = userId,
|
|
Phone = resolvedPhone,
|
|
Name = "Video Student"
|
|
}.WithTestPassword(),
|
|
new TenantMembership
|
|
{
|
|
TenantId = tenantId,
|
|
UserId = userId,
|
|
Role = TenantRole.Student,
|
|
Status = MembershipStatus.Active
|
|
});
|
|
return (tenantId, userId, resolvedPhone);
|
|
}
|
|
|
|
private static async Task LoginAsync(
|
|
HttpClient client,
|
|
(Guid TenantId, Guid UserId, string Phone) seed)
|
|
{
|
|
client.UseAccessToken(await client.LoginAsTenantAsync(seed.TenantId, seed.Phone));
|
|
}
|
|
} |