using DramaLing.Api.Tests.Integration.Fixtures; using System.Net; using System.Net.Http.Json; using System.Text.Json; namespace DramaLing.Api.Tests.Integration.EndToEnd; /// /// 使用者資料隔離測試 /// 驗證多用戶環境下的資料安全和隔離機制 /// public class DataIsolationTests : IntegrationTestBase { public DataIsolationTests(DramaLingWebApplicationFactory factory) : base(factory) { } [Fact] public async Task UserFlashcards_ShouldBeCompletelyIsolated() { // Arrange var user1Client = CreateTestUser1Client(); var user2Client = CreateTestUser2Client(); // Act: 兩個用戶分別取得詞卡列表 var user1Response = await user1Client.GetAsync("/api/flashcards"); var user2Response = await user2Client.GetAsync("/api/flashcards"); // Assert user1Response.StatusCode.Should().Be(HttpStatusCode.OK); user2Response.StatusCode.Should().Be(HttpStatusCode.OK); var user1Content = await user1Response.Content.ReadAsStringAsync(); var user2Content = await user2Response.Content.ReadAsStringAsync(); var user1Json = JsonSerializer.Deserialize(user1Content); var user2Json = JsonSerializer.Deserialize(user2Content); var user1Flashcards = user1Json.GetProperty("data").EnumerateArray().ToList(); var user2Flashcards = user2Json.GetProperty("data").EnumerateArray().ToList(); // 驗證 User1 只能看到自己的詞卡 (hello, beautiful) user1Flashcards.Should().HaveCount(2); user1Flashcards.Should().Contain(f => f.GetProperty("word").GetString() == "hello"); user1Flashcards.Should().Contain(f => f.GetProperty("word").GetString() == "beautiful"); // 驗證 User2 只能看到自己的詞卡 (sophisticated) user2Flashcards.Should().HaveCount(1); user2Flashcards.Should().Contain(f => f.GetProperty("word").GetString() == "sophisticated"); // 交叉驗證:確保絕對隔離 user1Flashcards.Should().NotContain(f => f.GetProperty("word").GetString() == "sophisticated"); user2Flashcards.Should().NotContain(f => f.GetProperty("word").GetString() == "hello"); user2Flashcards.Should().NotContain(f => f.GetProperty("word").GetString() == "beautiful"); } [Fact] public async Task ReviewData_ShouldBeIsolatedBetweenUsers() { // Arrange var user1Client = CreateTestUser1Client(); var user2Client = CreateTestUser2Client(); // Step 1: 用戶1進行複習 var user1FlashcardId = TestDataSeeder.TestFlashcard1Id; await user1Client.PostAsJsonAsync($"/api/flashcards/{user1FlashcardId}/review", new { confidence = 2, wasSkipped = false, responseTime = 2000 }); // Step 2: 檢查複習記錄隔離 using var context = GetDbContext(); var user1Reviews = context.FlashcardReviews .Where(r => r.UserId == TestDataSeeder.TestUser1Id) .ToList(); var user2Reviews = context.FlashcardReviews .Where(r => r.UserId == TestDataSeeder.TestUser2Id) .ToList(); // Assert: 驗證複習記錄隔離 user1Reviews.Should().HaveCountGreaterThan(0, "用戶1應該有複習記錄"); user2Reviews.Should().HaveCount(0, "用戶2不應該有複習記錄(在測試資料中)"); // 驗證 User1 的複習不會影響 User2 的資料 user1Reviews.Should().OnlyContain(r => r.UserId == TestDataSeeder.TestUser1Id); } [Fact] public async Task CreateFlashcard_ShouldOnlyBeAccessibleByOwner() { // Arrange var user1Client = CreateTestUser1Client(); var user2Client = CreateTestUser2Client(); // Step 1: User1 建立新詞卡 var newFlashcard = new { word = "isolation-test", translation = "隔離測試", definition = "A test for data isolation", partOfSpeech = "noun" }; var createResponse = await user1Client.PostAsJsonAsync("/api/flashcards", newFlashcard); createResponse.StatusCode.Should().Be(HttpStatusCode.OK); var createContent = await createResponse.Content.ReadAsStringAsync(); var createJson = JsonSerializer.Deserialize(createContent); var newFlashcardId = createJson.GetProperty("data").GetProperty("id").GetString(); // Step 2: User2 嘗試存取 User1 的詞卡 var accessResponse = await user2Client.GetAsync($"/api/flashcards/{newFlashcardId}"); // Assert: User2 應該無法存取 User1 的詞卡 accessResponse.StatusCode.Should().Be(HttpStatusCode.NotFound, "用戶不應該能存取其他用戶的詞卡"); // Step 3: User1 應該能正常存取自己的詞卡 var ownerAccessResponse = await user1Client.GetAsync($"/api/flashcards/{newFlashcardId}"); ownerAccessResponse.StatusCode.Should().Be(HttpStatusCode.OK, "用戶應該能存取自己的詞卡"); } [Fact] public async Task ReviewStats_ShouldBeUserSpecific() { // Arrange var user1Client = CreateTestUser1Client(); var user2Client = CreateTestUser2Client(); // Act: 獲取各用戶的複習統計 var user1StatsResponse = await user1Client.GetAsync("/api/flashcards/review-stats"); var user2StatsResponse = await user2Client.GetAsync("/api/flashcards/review-stats"); // Assert user1StatsResponse.StatusCode.Should().Be(HttpStatusCode.OK); user2StatsResponse.StatusCode.Should().Be(HttpStatusCode.OK); var user1StatsContent = await user1StatsResponse.Content.ReadAsStringAsync(); var user2StatsContent = await user2StatsResponse.Content.ReadAsStringAsync(); // 統計資料應該不同 (因為用戶有不同的詞卡和複習歷史) user1StatsContent.Should().NotBe(user2StatsContent, "不同用戶的統計資料應該不同"); // 解析並驗證統計資料結構 var user1Stats = JsonSerializer.Deserialize(user1StatsContent); var user2Stats = JsonSerializer.Deserialize(user2StatsContent); user1Stats.GetProperty("success").GetBoolean().Should().BeTrue(); user2Stats.GetProperty("success").GetBoolean().Should().BeTrue(); } [Fact] public async Task MasteredFlashcards_ShouldOnlyAffectOwner() { // Arrange var user1Client = CreateTestUser1Client(); var user2Client = CreateTestUser2Client(); var user1FlashcardId = TestDataSeeder.TestFlashcard1Id; // Step 1: User1 標記詞卡為已掌握 var masteredResponse = await user1Client.PostAsync($"/api/flashcards/{user1FlashcardId}/mastered", null); masteredResponse.StatusCode.Should().Be(HttpStatusCode.OK); // Step 2: 驗證只影響 User1 的複習間隔 using var context = GetDbContext(); var user1Review = context.FlashcardReviews .FirstOrDefault(r => r.FlashcardId == user1FlashcardId && r.UserId == TestDataSeeder.TestUser1Id); var user2Reviews = context.FlashcardReviews .Where(r => r.UserId == TestDataSeeder.TestUser2Id) .ToList(); // Assert user1Review.Should().NotBeNull("User1 應該有複習記錄"); user1Review!.SuccessCount.Should().BeGreaterThan(0, "User1 的成功次數應該增加"); // User2 的複習記錄不應受影響 user2Reviews.Should().NotContain(r => r.FlashcardId == user1FlashcardId, "User2 不應該有 User1 詞卡的複習記錄"); } }