182 lines
7.7 KiB
C#
182 lines
7.7 KiB
C#
using DramaLing.Api.Tests.Integration.Fixtures;
|
||
using System.Net;
|
||
using System.Net.Http.Json;
|
||
using System.Text.Json;
|
||
|
||
namespace DramaLing.Api.Tests.Integration.EndToEnd;
|
||
|
||
/// <summary>
|
||
/// 使用者資料隔離測試
|
||
/// 驗證多用戶環境下的資料安全和隔離機制
|
||
/// </summary>
|
||
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<JsonElement>(user1Content);
|
||
var user2Json = JsonSerializer.Deserialize<JsonElement>(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<JsonElement>(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<JsonElement>(user1StatsContent);
|
||
var user2Stats = JsonSerializer.Deserialize<JsonElement>(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 詞卡的複習記錄");
|
||
}
|
||
} |