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>( "/api/student/videos/search?keyword=透视", JsonOptions); var questionVideos = await client.GetFromJsonAsync>( $"/api/student/questions/videos?questionId={questionId}", JsonOptions); var batch = await (await client.PostAsJsonAsync( "/api/student/questions/videos/batch", new QuestionVideoQueryDto { QuestionIds = [questionId] })) .Content .ReadFromJsonAsync>(JsonOptions); var playResponse = await client.PostAsJsonAsync( "/api/student/videos/play", new VideoPlayDto { VideoId = videoId, QuestionId = questionId }); var play = await playResponse.Content.ReadFromJsonAsync(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(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(); 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)); } }