-
Notifications
You must be signed in to change notification settings - Fork 0
test: cover API clients, zip and seal pipeline #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,243 @@ | ||
| using System.Net; | ||
| using E4A.PostGuard.Api; | ||
| using E4A.PostGuard.Exceptions; | ||
| using E4A.PostGuard.Models; | ||
| using E4A.PostGuard.Tests.TestHelpers; | ||
|
|
||
| namespace E4A.PostGuard.Tests; | ||
|
|
||
| public class CryptifyClientTests | ||
| { | ||
| private const string BaseUrl = "https://cryptify.postguard.eu"; | ||
| private const int ChunkSize = 1024 * 1024; // mirrors CryptifyClient.ChunkSize | ||
|
|
||
| private static readonly Dictionary<string, string> InitToken = | ||
| new() { ["cryptifytoken"] = "token-0" }; | ||
|
|
||
| private static RecipientBuilder Email(string email) => | ||
| new(email, RecipientBaseType.Email); | ||
|
|
||
| private static (CryptifyClient Client, RecordingHttpMessageHandler Handler) NewClient() | ||
| { | ||
| var handler = new RecordingHttpMessageHandler(); | ||
| return (new CryptifyClient(new HttpClient(handler), BaseUrl), handler); | ||
| } | ||
|
|
||
| private static HttpResponseMessage TokenResponse(string token) | ||
| { | ||
| var response = new HttpResponseMessage(HttpStatusCode.OK) | ||
| { | ||
| Content = new StringContent(""), | ||
| }; | ||
| response.Headers.TryAddWithoutValidation("cryptifytoken", token); | ||
| return response; | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task SingleChunk_WiresInitChunkFinalize() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler | ||
| .EnqueueJson("""{"uuid":"abc-123"}""", headers: InitToken) | ||
| .Enqueue(TokenResponse("token-1")) | ||
| .Enqueue(TokenResponse("token-2")); | ||
|
|
||
| var data = new byte[100]; | ||
| var uuid = await client.UploadAsync(data, [Email("alice@example.com")], notify: null); | ||
|
|
||
| Assert.Equal("abc-123", uuid); | ||
| Assert.Equal(3, handler.Requests.Count); | ||
|
|
||
| Assert.Equal(HttpMethod.Post, handler.Requests[0].Method); | ||
| Assert.Equal($"{BaseUrl}/fileupload/init", handler.Requests[0].Uri.ToString()); | ||
|
|
||
| Assert.Equal(HttpMethod.Put, handler.Requests[1].Method); | ||
| Assert.Equal($"{BaseUrl}/fileupload/abc-123", handler.Requests[1].Uri.ToString()); | ||
|
|
||
| Assert.Equal(HttpMethod.Post, handler.Requests[2].Method); | ||
| Assert.Equal($"{BaseUrl}/fileupload/finalize/abc-123", handler.Requests[2].Uri.ToString()); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task ChunkContentRange_IsFormattedPerChunk() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler | ||
| .EnqueueJson("""{"uuid":"u"}""", headers: InitToken) | ||
| .Enqueue(TokenResponse("t1")) | ||
| .Enqueue(TokenResponse("t2")) | ||
| .Enqueue(TokenResponse("t3")); | ||
|
|
||
| // 1.5 chunks → two PUTs. | ||
| var total = ChunkSize + 100; | ||
| var uuid = await client.UploadAsync(new byte[total], [Email("a@b.com")], notify: null); | ||
| Assert.Equal("u", uuid); | ||
|
|
||
| var puts = handler.Requests.Where(r => r.Method == HttpMethod.Put).ToList(); | ||
| Assert.Equal(2, puts.Count); | ||
|
|
||
| // NOTE: the current SDK emits an exclusive range end (`end = offset + len`). | ||
| // encryption4all/postguard-dotnet#34 changes this to an inclusive end; that | ||
| // PR must update these two assertions when it lands. | ||
| Assert.Equal($"bytes 0-{ChunkSize}/*", puts[0].ContentRange); | ||
| Assert.Equal($"bytes {ChunkSize}-{total}/*", puts[1].ContentRange); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Finalize_SendsTotalSizeContentRange() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler | ||
| .EnqueueJson("""{"uuid":"u"}""", headers: InitToken) | ||
| .Enqueue(TokenResponse("t1")) | ||
| .Enqueue(TokenResponse("t2")); | ||
|
|
||
| var total = 4242; | ||
| await client.UploadAsync(new byte[total], [Email("a@b.com")], notify: null); | ||
|
|
||
| var finalize = handler.Requests.Single(r => r.Uri.ToString().Contains("/finalize/")); | ||
| Assert.Equal($"bytes */{total}", finalize.ContentRange); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task TokenRotates_AcrossChunksAndFinalize() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler | ||
| .EnqueueJson("""{"uuid":"u"}""", headers: InitToken) // init -> token-0 | ||
| .Enqueue(TokenResponse("token-1")) // chunk 1 -> token-1 | ||
| .Enqueue(TokenResponse("token-2")) // chunk 2 -> token-2 | ||
| .Enqueue(TokenResponse("token-3")); // finalize ok | ||
|
|
||
| await client.UploadAsync(new byte[ChunkSize + 1], [Email("a@b.com")], notify: null); | ||
|
|
||
| // init carries no token; each subsequent request carries the token from | ||
| // the previous response. | ||
| Assert.Null(handler.Requests[0].CryptifyToken); | ||
| Assert.Equal("token-0", handler.Requests[1].CryptifyToken); // first chunk uses init token | ||
| Assert.Equal("token-1", handler.Requests[2].CryptifyToken); // second chunk uses chunk-1 token | ||
| Assert.Equal("token-2", handler.Requests[3].CryptifyToken); // finalize uses last chunk token | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Init_JoinsRecipientEmailsWithComma() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler | ||
| .EnqueueJson("""{"uuid":"u"}""", headers: InitToken) | ||
| .Enqueue(TokenResponse("t1")) | ||
| .Enqueue(TokenResponse("t2")); | ||
|
|
||
| await client.UploadAsync( | ||
| new byte[10], | ||
| [Email("alice@example.com"), Email("bob@example.com")], | ||
| notify: null); | ||
|
|
||
| Assert.Contains("alice@example.com,bob@example.com", handler.Requests[0].BodyText); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Notify_DefaultsToSilentUpload() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler | ||
| .EnqueueJson("""{"uuid":"u"}""", headers: InitToken) | ||
| .Enqueue(TokenResponse("t1")) | ||
| .Enqueue(TokenResponse("t2")); | ||
|
|
||
| await client.UploadAsync(new byte[10], [Email("a@b.com")], notify: null); | ||
|
|
||
| var body = handler.Requests[0].BodyText; | ||
| Assert.Contains("\"confirm\":false", body); | ||
| Assert.Contains("\"notifyRecipients\":false", body); | ||
| Assert.Contains("\"mailLang\":\"EN\"", body); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Notify_PropagatesOptionsToInitBody() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler | ||
| .EnqueueJson("""{"uuid":"u"}""", headers: InitToken) | ||
| .Enqueue(TokenResponse("t1")) | ||
| .Enqueue(TokenResponse("t2")); | ||
|
|
||
| var notify = new NotifyOptions | ||
| { | ||
| Recipients = true, | ||
| Sender = true, | ||
| Message = "hello", | ||
| Language = "NL", | ||
| }; | ||
| await client.UploadAsync(new byte[10], [Email("a@b.com")], notify); | ||
|
|
||
| var body = handler.Requests[0].BodyText; | ||
| Assert.Contains("\"confirm\":true", body); | ||
| Assert.Contains("\"notifyRecipients\":true", body); | ||
| Assert.Contains("\"mailContent\":\"hello\"", body); | ||
| Assert.Contains("\"mailLang\":\"NL\"", body); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task NullUuid_ThrowsPostGuardException() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler.EnqueueJson("""{"uuid":null}""", headers: InitToken); | ||
|
|
||
| var ex = await Assert.ThrowsAsync<PostGuardException>( | ||
| () => client.UploadAsync(new byte[10], [Email("a@b.com")], notify: null)); | ||
| Assert.Contains("uuid", ex.Message); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task MissingInitToken_ThrowsPostGuardException() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler.EnqueueJson("""{"uuid":"u"}"""); // no cryptifytoken header | ||
|
|
||
| var ex = await Assert.ThrowsAsync<PostGuardException>( | ||
| () => client.UploadAsync(new byte[10], [Email("a@b.com")], notify: null)); | ||
| Assert.Contains("cryptifytoken", ex.Message); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task MissingChunkToken_ThrowsPostGuardException() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler | ||
| .EnqueueJson("""{"uuid":"u"}""", headers: InitToken) | ||
| .Enqueue(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("") }); // no token | ||
|
|
||
| var ex = await Assert.ThrowsAsync<PostGuardException>( | ||
| () => client.UploadAsync(new byte[10], [Email("a@b.com")], notify: null)); | ||
| Assert.Contains("cryptifytoken", ex.Message); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task NonSuccessInit_ThrowsNetworkExceptionWithUrl() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler.EnqueueJson("nope", HttpStatusCode.InternalServerError); | ||
|
|
||
| var ex = await Assert.ThrowsAsync<NetworkException>( | ||
| () => client.UploadAsync(new byte[10], [Email("a@b.com")], notify: null)); | ||
| Assert.Equal(500, ex.StatusCode); | ||
| Assert.Contains("/fileupload/init", ex.Url); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Ctor_TrimsTrailingSlashFromUrl() | ||
| { | ||
| var handler = new RecordingHttpMessageHandler(); | ||
| handler | ||
| .EnqueueJson("""{"uuid":"u"}""", headers: InitToken) | ||
| .Enqueue(TokenResponse("t1")) | ||
| .Enqueue(TokenResponse("t2")); | ||
| var client = new CryptifyClient(new HttpClient(handler), BaseUrl + "/"); | ||
|
|
||
| await client.UploadAsync(new byte[10], [Email("a@b.com")], notify: null); | ||
|
|
||
| // No double slash in the init URL. | ||
| Assert.Equal($"{BaseUrl}/fileupload/init", handler.Requests[0].Uri.ToString()); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| using System.Net; | ||
| using System.Text.Json; | ||
| using E4A.PostGuard.Api; | ||
| using E4A.PostGuard.Exceptions; | ||
| using E4A.PostGuard.Tests.TestHelpers; | ||
|
|
||
| namespace E4A.PostGuard.Tests; | ||
|
|
||
| public class PkgClientTests | ||
| { | ||
| private const string BaseUrl = "https://pkg.postguard.eu"; | ||
|
|
||
| private static (PkgClient Client, RecordingHttpMessageHandler Handler) NewClient() | ||
| { | ||
| var handler = new RecordingHttpMessageHandler(); | ||
| return (new PkgClient(new HttpClient(handler), BaseUrl), handler); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task FetchMpk_GetsParametersEndpoint() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler.EnqueueJson("""{"publicKey":"AAEC"}"""); | ||
|
|
||
| await client.FetchMpkJsonAsync(); | ||
|
|
||
| Assert.Equal(HttpMethod.Get, handler.Requests[0].Method); | ||
| Assert.Equal($"{BaseUrl}/v2/parameters", handler.Requests[0].Uri.ToString()); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task FetchMpk_ReturnsSerializedStringValue() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler.EnqueueJson("""{"publicKey":"AAEC","ignored":42}"""); | ||
|
|
||
| var mpk = await client.FetchMpkJsonAsync(); | ||
|
|
||
| // A quoted base64 string is returned verbatim as a JSON string literal. | ||
| Assert.Equal("\"AAEC\"", mpk); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task FetchMpk_ReturnsSerializedObjectValue() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler.EnqueueJson("""{"publicKey":{"alg":"kem","key":"AAEC"}}"""); | ||
|
|
||
| var mpk = await client.FetchMpkJsonAsync(); | ||
|
|
||
| // The publicKey sub-object is preserved as valid JSON. | ||
| using var doc = JsonDocument.Parse(mpk); | ||
| Assert.Equal("kem", doc.RootElement.GetProperty("alg").GetString()); | ||
| Assert.Equal("AAEC", doc.RootElement.GetProperty("key").GetString()); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task FetchMpk_MissingPublicKey_Throws() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler.EnqueueJson("""{"somethingElse":"x"}"""); | ||
|
|
||
| await Assert.ThrowsAsync<KeyNotFoundException>(() => client.FetchMpkJsonAsync()); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task FetchMpk_NonSuccess_ThrowsNetworkException() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler.EnqueueJson("server error", HttpStatusCode.BadGateway); | ||
|
|
||
| var ex = await Assert.ThrowsAsync<NetworkException>(() => client.FetchMpkJsonAsync()); | ||
| Assert.Equal(502, ex.StatusCode); | ||
| Assert.Contains("/v2/parameters", ex.Url); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task FetchSigningKeys_PostsWithBearerAuthAndBody() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler.EnqueueJson("""{"pubSignKey":{"k":"pub"}}"""); | ||
|
|
||
| await client.FetchSigningKeysAsync("my-api-key"); | ||
|
|
||
| var req = handler.Requests[0]; | ||
| Assert.Equal(HttpMethod.Post, req.Method); | ||
| Assert.Equal($"{BaseUrl}/v2/irma/sign/key", req.Uri.ToString()); | ||
| Assert.Equal("Bearer my-api-key", req.Authorization); | ||
| Assert.Contains("pbdf.sidn-pbdf.email.email", req.BodyText); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task FetchSigningKeys_ReturnsPubAndPriv() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler.EnqueueJson("""{"pubSignKey":{"k":"pub"},"privSignKey":{"k":"priv"}}"""); | ||
|
|
||
| var (pub, priv) = await client.FetchSigningKeysAsync("key"); | ||
|
|
||
| using var pubDoc = JsonDocument.Parse(pub); | ||
| Assert.Equal("pub", pubDoc.RootElement.GetProperty("k").GetString()); | ||
|
|
||
| Assert.NotNull(priv); | ||
| using var privDoc = JsonDocument.Parse(priv!); | ||
| Assert.Equal("priv", privDoc.RootElement.GetProperty("k").GetString()); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task FetchSigningKeys_AbsentPrivKey_ReturnsNull() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler.EnqueueJson("""{"pubSignKey":{"k":"pub"}}"""); | ||
|
|
||
| var (pub, priv) = await client.FetchSigningKeysAsync("key"); | ||
|
|
||
| Assert.NotNull(pub); | ||
| Assert.Null(priv); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task FetchSigningKeys_NullPrivKey_ReturnsNull() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler.EnqueueJson("""{"pubSignKey":{"k":"pub"},"privSignKey":null}"""); | ||
|
|
||
| var (_, priv) = await client.FetchSigningKeysAsync("key"); | ||
|
|
||
| Assert.Null(priv); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task FetchSigningKeys_MissingPubKey_Throws() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler.EnqueueJson("""{"privSignKey":{"k":"priv"}}"""); | ||
|
|
||
| await Assert.ThrowsAsync<KeyNotFoundException>(() => client.FetchSigningKeysAsync("key")); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task FetchSigningKeys_NonSuccess_ThrowsNetworkException() | ||
| { | ||
| var (client, handler) = NewClient(); | ||
| handler.EnqueueJson("unauthorized", HttpStatusCode.Unauthorized); | ||
|
|
||
| var ex = await Assert.ThrowsAsync<NetworkException>( | ||
| () => client.FetchSigningKeysAsync("bad-key")); | ||
| Assert.Equal(401, ex.StatusCode); | ||
| Assert.Contains("/v2/irma/sign/key", ex.Url); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non-blocking / awareness only: this asserts the current exclusive Content-Range end (
bytes 0-1048576/*). Open PR #34 changes the SDK to an inclusive end (bytes 0-1048575/*); when #34 merges, this assertion and the one below will need updating. The author already flagged it in the inline NOTE — no action needed in this PR.