SnapshotService.CreateAndUploadAsync lädt in zwei Schritten hoch: Schritt 1 holt einen Code vom Server, Schritt 2 verschlüsselt den Sync-Schlüssel mit diesem Code und lädt erneut hoch. SnapshotStore.Store() vergab bei jedem Aufruf bedingungslos einen neuen Zufallscode - der dem Nutzer am Ende angezeigte Code war dadurch nie derselbe, mit dem der Schlüssel tatsächlich verschlüsselt wurde. Jede Kopplung musste deterministisch an der Schlüssel-Entschlüsselung scheitern. SnapshotUploadRequest bekommt ein optionales Code-Feld; Store() aktualisiert bei vorhandenem, passendem Code denselben Eintrag statt einen neuen mit neuem Code anzulegen. Betrifft LehrerApp.Api - der Server muss neu deployt werden. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
90 lines
3.3 KiB
C#
90 lines
3.3 KiB
C#
using LehrerApp.Sync.Models;
|
|
using Xunit;
|
|
|
|
namespace LehrerApp.Api.Tests;
|
|
|
|
public sealed class SnapshotStoreTests
|
|
{
|
|
[Fact]
|
|
public void Store_ZweiterAufrufOhneCode_ErzeugtNeuenCode()
|
|
{
|
|
using var temp = new TempSnapshotStore();
|
|
|
|
var first = temp.Store.Store("user1", new SnapshotUploadRequest { EncryptedPayload = "p1" });
|
|
var second = temp.Store.Store("user1", new SnapshotUploadRequest { EncryptedPayload = "p2" });
|
|
|
|
Assert.NotEqual(first.Code, second.Code);
|
|
}
|
|
|
|
/// Regression: SnapshotService.CreateAndUploadAsync lädt in zwei Schritten hoch — Schritt 1
|
|
/// ohne EncryptedSyncKey (holt einen Code vom Server), Schritt 2 mit dem gerade erst mit
|
|
/// diesem Code verschlüsselten Schlüssel. Ohne den zurückgereichten Code erzeugte Store() bei
|
|
/// jedem Aufruf einen NEUEN Zufallscode — der an den Nutzer angezeigte Code (aus Schritt 2)
|
|
/// passte dann nie zu dem Code, mit dem der Schlüssel tatsächlich verschlüsselt wurde, und das
|
|
/// Einlösen auf dem zweiten Gerät schlug beim Schlüssel-Entschlüsseln immer fehl.
|
|
[Fact]
|
|
public void Store_ZweiterAufrufMitCodeDesErstenSchritts_AktualisiertDenselbenEintrag()
|
|
{
|
|
using var temp = new TempSnapshotStore();
|
|
|
|
var step1 = temp.Store.Store("user1", new SnapshotUploadRequest { EncryptedPayload = "payload" });
|
|
var step2 = temp.Store.Store("user1", new SnapshotUploadRequest
|
|
{
|
|
EncryptedPayload = "payload", EncryptedSyncKey = "encrypted-key", Code = step1.Code,
|
|
});
|
|
|
|
Assert.Equal(step1.Code, step2.Code);
|
|
|
|
var retrieved = temp.Store.Retrieve("user1", step2.Code);
|
|
Assert.NotNull(retrieved);
|
|
Assert.Equal("encrypted-key", retrieved!.EncryptedSyncKey);
|
|
}
|
|
|
|
[Fact]
|
|
public void Store_CodeGehoertZuAnderemNutzer_ErzeugtStattdessenNeuenEintrag()
|
|
{
|
|
using var temp = new TempSnapshotStore();
|
|
var step1 = temp.Store.Store("user1", new SnapshotUploadRequest { EncryptedPayload = "payload" });
|
|
|
|
var result = temp.Store.Store("user2", new SnapshotUploadRequest
|
|
{
|
|
EncryptedPayload = "payload", EncryptedSyncKey = "encrypted-key", Code = step1.Code,
|
|
});
|
|
|
|
Assert.NotEqual(step1.Code, result.Code);
|
|
Assert.NotNull(temp.Store.Retrieve("user2", result.Code));
|
|
}
|
|
|
|
[Fact]
|
|
public void Retrieve_LoeschtDenEintragNachDemErstenAbruf()
|
|
{
|
|
using var temp = new TempSnapshotStore();
|
|
var uploaded = temp.Store.Store("user1", new SnapshotUploadRequest { EncryptedPayload = "payload" });
|
|
|
|
var first = temp.Store.Retrieve("user1", uploaded.Code);
|
|
var second = temp.Store.Retrieve("user1", uploaded.Code);
|
|
|
|
Assert.NotNull(first);
|
|
Assert.Null(second);
|
|
}
|
|
|
|
private sealed class TempSnapshotStore : IDisposable
|
|
{
|
|
private readonly string _directory = Path.Combine(
|
|
Path.GetTempPath(), $"lehrerapp-api-tests-{Guid.NewGuid():N}");
|
|
public LehrerApp.Api.SnapshotStore Store { get; }
|
|
|
|
public TempSnapshotStore()
|
|
{
|
|
Directory.CreateDirectory(_directory);
|
|
Store = new LehrerApp.Api.SnapshotStore(_directory);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Store.Dispose();
|
|
if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true);
|
|
}
|
|
}
|
|
}
|