Files
WebDrop/server/tests/WebDrop.Api.Tests/ShareTests.cs
T

171 lines
9.6 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");
}
[Fact]
public async Task ResumeWithoutAnInitialDownloadCannotBypassShareDownloadLimit()
{
var share = new ShareClient(NewId());
using (var created = await CreateShare(share)) Assert.Equal(HttpStatusCode.Created, created.StatusCode);
string manifestId = NewId();
using (var manifest = await SendFrame(share, share.Owner, Frame(manifestId, 0, FinalEnc, EncPayload)))
Assert.Equal(HttpStatusCode.Created, manifest.StatusCode);
string itemId = NewId();
using (var first = await SendFrame(share, share.Owner, Frame(itemId, 0, GatedEnc, EncPayload), "1"))
Assert.Equal(HttpStatusCode.Created, first.StatusCode);
using (var last = await SendFrame(share, share.Owner, Frame(itemId, 1, GatedFinalEnc, EncPayload)))
Assert.Equal(HttpStatusCode.Created, last.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);
string path = $"/api/v1/shares/{share.Id}/items/{itemId}/frames";
await AssertErrorAsync(await server.Http.SendAsync(server.Req(HttpMethod.Get, path + "?from=1", share.OtherVisitor)), 410, "gone");
var (initialResponse, initialBytes) = await server.GetBytesAsync(path, share.Visitor);
using (initialResponse) Assert.Equal(Frame(itemId, 0, GatedEnc, EncPayload, 0x42).Concat(Frame(itemId, 1, GatedFinalEnc, EncPayload, 0x42)), initialBytes);
var (resumeResponse, resumedBytes) = await server.GetBytesAsync(path + "?from=1", share.Visitor);
using (resumeResponse) Assert.Equal(Frame(itemId, 1, GatedFinalEnc, EncPayload, 0x42), resumedBytes);
using var infoResponse = await server.Http.SendAsync(server.Req(HttpMethod.Get, $"/api/v1/shares/{share.Id}", share.CreatorReader));
var item = (await ReadJsonAsync(infoResponse)).GetProperty("items").EnumerateArray().Single();
Assert.Equal(0, item.GetProperty("downloads_left").GetInt32());
}
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);
}
}