140 lines
7.5 KiB
C#
140 lines
7.5 KiB
C#
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);
|
|
}
|
|
}
|