feat(server): implement sealed share lifecycle
Signed-off-by: JustMe <mr.toing@outlook.com>
This commit is contained in:
1 parent
e02e024605
commit
42b93ae2d8
45 files changed
+6327
No files matched your search
@@ -0,0 +1,139 @@
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using WebDrop.Core;
|
||||
using static WebDrop.Api.Tests.ServerFixture;
|
||||
|
||||
namespace WebDrop.Api.Tests;
|
||||
|
||||
public sealed class ShareTests(ServerFixture server) : IClassFixture<ServerFixture>
|
||||
{
|
||||
private sealed class ShareClient(string id)
|
||||
{
|
||||
public string Id { get; } = id;
|
||||
public byte[] ReadToken { get; } = RandomNumberGenerator.GetBytes(32);
|
||||
public byte[] OwnerToken { get; } = RandomNumberGenerator.GetBytes(32);
|
||||
public byte[] OwnerEndpoint { get; } = RandomNumberGenerator.GetBytes(32);
|
||||
public byte[] VisitorEndpoint { get; } = RandomNumberGenerator.GetBytes(32);
|
||||
public byte[] OtherEndpoint { get; } = RandomNumberGenerator.GetBytes(32);
|
||||
|
||||
public Who Owner => new("owner", OwnerToken, OwnerEndpoint);
|
||||
public Who CreatorReader => new("read", ReadToken, OwnerEndpoint);
|
||||
public Who Visitor => new("read", ReadToken, VisitorEndpoint);
|
||||
public Who OtherVisitor => new("read", ReadToken, OtherEndpoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DraftSealsIntoReadOnlyShareWithVisitorAndDownloadLimits()
|
||||
{
|
||||
var share = new ShareClient(NewId());
|
||||
using (var req = server.Req(HttpMethod.Post, "/api/v1/shares", share.Owner, JsonBody(new
|
||||
{
|
||||
id = share.Id,
|
||||
read_hash = B64(TokenHash.Compute(share.ReadToken)),
|
||||
owner_hash = B64(TokenHash.Compute(share.OwnerToken)),
|
||||
settings = new { ttl_seconds = 3600, max_visitors = 1, file_max_downloads = 1 },
|
||||
})))
|
||||
using (var created = await server.Http.SendAsync(req))
|
||||
{
|
||||
Assert.Equal(HttpStatusCode.Created, created.StatusCode);
|
||||
Assert.Equal("draft", (await ReadJsonAsync(created)).GetProperty("state").GetString());
|
||||
}
|
||||
|
||||
using (var draftRead = await server.Http.SendAsync(server.Req(HttpMethod.Get, $"/api/v1/shares/{share.Id}", share.Owner)))
|
||||
Assert.Equal(HttpStatusCode.OK, draftRead.StatusCode);
|
||||
await AssertErrorAsync(await server.Http.SendAsync(server.Req(HttpMethod.Get,
|
||||
$"/api/v1/shares/{share.Id}", share.CreatorReader)), 404, "not_found");
|
||||
|
||||
string manifestId = NewId();
|
||||
byte[] manifest = Frame(manifestId, 0, FinalEnc, EncPayload, 0x11);
|
||||
using (var putManifest = await server.Http.SendAsync(server.Req(HttpMethod.Post,
|
||||
$"/api/v1/shares/{share.Id}/frames", share.Owner, Bytes(manifest))))
|
||||
Assert.Equal(HttpStatusCode.Created, putManifest.StatusCode);
|
||||
|
||||
string fileId = NewId();
|
||||
byte[] first = Frame(fileId, 0, GatedEnc, EncPayload, 0x21);
|
||||
byte[] last = Frame(fileId, 1, GatedFinalEnc, EncPayload, 0x22);
|
||||
using (var putFirst = await SendFrame(share, share.Owner, first, "1"))
|
||||
Assert.Equal(HttpStatusCode.Created, putFirst.StatusCode);
|
||||
using (var putLast = await SendFrame(share, share.Owner, last))
|
||||
Assert.Equal(HttpStatusCode.Created, putLast.StatusCode);
|
||||
|
||||
using (var finish = await server.Http.SendAsync(server.Req(HttpMethod.Post,
|
||||
$"/api/v1/shares/{share.Id}/finish", share.Owner, JsonBody(new { manifest_item_id = manifestId }))))
|
||||
{
|
||||
Assert.Equal(HttpStatusCode.OK, finish.StatusCode);
|
||||
var info = await ReadJsonAsync(finish);
|
||||
Assert.Equal("sealed", info.GetProperty("state").GetString());
|
||||
Assert.Equal(manifestId, info.GetProperty("manifest_item_id").GetString());
|
||||
Assert.Single(info.GetProperty("items").EnumerateArray()); // manifest is not a downloadable file
|
||||
}
|
||||
|
||||
await AssertErrorAsync(await SendFrame(share, share.Owner, Frame(NewId(), 0, FinalEnc, EncPayload)), 409, "sealed");
|
||||
using (var invalidRole = await server.Http.SendAsync(server.Req(HttpMethod.Get,
|
||||
$"/api/v1/shares/{share.Id}", new Who("write", share.ReadToken, share.OtherEndpoint))))
|
||||
await AssertErrorAsync(invalidRole, 400, "bad_request");
|
||||
|
||||
var (manifestResponse, manifestBody) = await server.GetBytesAsync(
|
||||
$"/api/v1/shares/{share.Id}/manifest", share.CreatorReader);
|
||||
using (manifestResponse) Assert.Equal(manifest, manifestBody);
|
||||
|
||||
var (visitorManifestResponse, visitorManifest) = await server.GetBytesAsync(
|
||||
$"/api/v1/shares/{share.Id}/manifest", share.Visitor);
|
||||
using (visitorManifestResponse) Assert.Equal(manifest, visitorManifest);
|
||||
await AssertErrorAsync(await server.Http.SendAsync(server.Req(HttpMethod.Get,
|
||||
$"/api/v1/shares/{share.Id}/manifest", share.OtherVisitor)), 403, "visitors_full");
|
||||
|
||||
string downloadPath = $"/api/v1/shares/{share.Id}/items/{fileId}/frames";
|
||||
var (downloadResponse, downloaded) = await server.GetBytesAsync(downloadPath, share.Visitor);
|
||||
using (downloadResponse) Assert.Equal(first.Concat(last), downloaded);
|
||||
await AssertErrorAsync(await server.Http.SendAsync(server.Req(HttpMethod.Get, downloadPath, share.Visitor)), 410, "gone");
|
||||
var (resumeResponse, resumed) = await server.GetBytesAsync(downloadPath + "?from=1", share.Visitor);
|
||||
using (resumeResponse) Assert.Equal(last, resumed);
|
||||
|
||||
using var finalInfo = await server.Http.SendAsync(server.Req(HttpMethod.Get,
|
||||
$"/api/v1/shares/{share.Id}", share.CreatorReader));
|
||||
var file = (await ReadJsonAsync(finalInfo)).GetProperty("items").EnumerateArray().Single();
|
||||
Assert.Equal(0, file.GetProperty("downloads_left").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShareAndChannelIdsShareOneUniqueNamespace()
|
||||
{
|
||||
var share = new ShareClient(NewId());
|
||||
using (var created = await CreateShare(share)) Assert.Equal(HttpStatusCode.Created, created.StatusCode);
|
||||
|
||||
byte[] channelOwnerToken = RandomNumberGenerator.GetBytes(32);
|
||||
var channelOwner = new Who("owner", channelOwnerToken, RandomNumberGenerator.GetBytes(32));
|
||||
using (var collision = await server.Http.SendAsync(server.Req(HttpMethod.Post, "/api/v1/channels", channelOwner,
|
||||
JsonBody(new { id = share.Id, mode = "raw", owner_hash = B64(TokenHash.Compute(channelOwnerToken)) }))))
|
||||
await AssertErrorAsync(collision, 409, "conflict");
|
||||
|
||||
string channelId = NewId();
|
||||
byte[] secondOwnerToken = RandomNumberGenerator.GetBytes(32);
|
||||
var secondOwner = new Who("owner", secondOwnerToken, RandomNumberGenerator.GetBytes(32));
|
||||
using (var channel = await server.Http.SendAsync(server.Req(HttpMethod.Post, "/api/v1/channels", secondOwner,
|
||||
JsonBody(new { id = channelId, mode = "raw", owner_hash = B64(TokenHash.Compute(secondOwnerToken)) }))))
|
||||
Assert.Equal(HttpStatusCode.Created, channel.StatusCode);
|
||||
|
||||
using var shareCollision = await CreateShare(new ShareClient(channelId));
|
||||
await AssertErrorAsync(shareCollision, 409, "conflict");
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> CreateShare(ShareClient share)
|
||||
{
|
||||
using var req = server.Req(HttpMethod.Post, "/api/v1/shares", share.Owner, JsonBody(new
|
||||
{
|
||||
id = share.Id,
|
||||
read_hash = B64(TokenHash.Compute(share.ReadToken)),
|
||||
owner_hash = B64(TokenHash.Compute(share.OwnerToken)),
|
||||
}));
|
||||
return await server.Http.SendAsync(req);
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> SendFrame(ShareClient share, Who who, byte[] bytes, string? maxDownloads = null)
|
||||
{
|
||||
using var req = server.Req(HttpMethod.Post, $"/api/v1/shares/{share.Id}/frames", who, Bytes(bytes));
|
||||
if (maxDownloads is not null) req.Headers.Add("X-WD-Max-Downloads", maxDownloads);
|
||||
return await server.Http.SendAsync(req);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user