Skip to content

Commit

Permalink
Merge pull request #53 from dlcs/feature/iiifCollectionPost
Browse files Browse the repository at this point in the history
Enabling POST for IIIF collections
  • Loading branch information
JackLewis-digirati authored Oct 1, 2024
2 parents 966675e + f23ac6e commit 0b2d019
Show file tree
Hide file tree
Showing 39 changed files with 1,787 additions and 53 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,10 @@ public class DatabaseCollection : ICollectionFixture<PresentationContextFixture>
{
public const string CollectionName = "Database Collection";
}

[CollectionDefinition(CollectionName)]
public class StorageCollection : ICollectionFixture<StorageFixture>
{
public const string CollectionName = "Storage Collection";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Text;
using API.Helpers;
using API.Infrastructure.IdGenerator;
using Amazon.S3;
using API.Tests.Integration.Infrastructure;
using Core.Response;
using FakeItEasy;
Expand All @@ -23,27 +24,31 @@
namespace API.Tests.Integration;

[Trait("Category", "Integration")]
[Collection(CollectionDefinitions.DatabaseCollection.CollectionName)]
[Collection(CollectionDefinitions.StorageCollection.CollectionName)]
public class ModifyCollectionTests : IClassFixture<PresentationAppFactory<Program>>
{
private readonly HttpClient httpClient;

private readonly PresentationContext dbContext;

private readonly IAmazonS3 amazonS3;

private const int Customer = 1;

private readonly string parent;

public ModifyCollectionTests(PresentationContextFixture dbFixture, PresentationAppFactory<Program> factory)
public ModifyCollectionTests(StorageFixture storageFixture, PresentationAppFactory<Program> factory)
{
dbContext = dbFixture.DbContext;
dbContext = storageFixture.DbFixture.DbContext;
amazonS3 = storageFixture.LocalStackFixture.AWSS3ClientFactory();

httpClient = factory.WithConnectionString(dbFixture.ConnectionString)
httpClient = factory.WithConnectionString(storageFixture.DbFixture.ConnectionString)
.WithLocalStack(storageFixture.LocalStackFixture)
.CreateClient(new WebApplicationFactoryClientOptions());

parent = dbContext.Collections.First(x => x.CustomerId == Customer && x.Slug == string.Empty).Id;

dbFixture.CleanUp();
storageFixture.DbFixture.CleanUp();
}

[Fact]
Expand Down Expand Up @@ -92,7 +97,7 @@ public async Task CreateCollection_CreatesCollection_WhenAllValuesProvided()
}

[Fact]
public async Task CreateCollection_FailsToCreateCollection_WhenIsStorageCollectionFalse()
public async Task CreateCollection_CreatesCollection_WhenIsStorageCollectionFalse()
{
// Arrange
var collection = new UpsertFlatCollection()
Expand All @@ -103,16 +108,41 @@ public async Task CreateCollection_FailsToCreateCollection_WhenIsStorageCollecti
},
Label = new LanguageMap("en", ["test collection"]),
Slug = "programmatic-child",
Parent = parent
Parent = parent,
Thumbnail = "some/thumbnail",
Tags = "some, tags",
ItemsOrder = 1,
};

var requestMessage = HttpRequestMessageBuilder.GetPrivateRequest(HttpMethod.Post, $"{Customer}/collections", JsonSerializer.Serialize(collection));

// Act
var response = await httpClient.AsCustomer(1).SendAsync(requestMessage);

var responseCollection = await response.ReadAsPresentationResponseAsync<PresentationCollection>();

var fromDatabase = dbContext.Collections.First(c =>
c.Id == responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last());

var fromS3 =
await amazonS3.GetObjectAsync(LocalStackFixture.StorageBucketName,
$"{Customer}/collections/{fromDatabase.Id}");

// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
response.StatusCode.Should().Be(HttpStatusCode.Created);
fromDatabase.Parent.Should().Be(parent);
fromDatabase.Label!.Values.First()[0].Should().Be("test collection");
fromDatabase.Slug.Should().Be("programmatic-child");
fromDatabase.ItemsOrder.Should().Be(1);
fromDatabase.Thumbnail.Should().Be("some/thumbnail");
fromDatabase.Tags.Should().Be("some, tags");
fromDatabase.IsPublic.Should().BeTrue();
fromDatabase.IsStorageCollection.Should().BeFalse();
fromDatabase.Modified.Should().Be(fromDatabase.Created);
responseCollection!.View!.PageSize.Should().Be(20);
responseCollection.View.Page.Should().Be(1);
responseCollection.View.Id.Should().Contain("?page=1&pageSize=20");
fromS3.Should().NotBeNull();
}

[Fact]
Expand Down Expand Up @@ -467,7 +497,7 @@ public async Task UpdateCollection_UpdatesCollection_WhenAllValuesProvidedWithou
fromDatabase.IsStorageCollection.Should().BeTrue();
}

[Fact]
[Fact (Skip = "Test to be updated to pass in https://github.com/dlcs/iiif-presentation/issues/27")]
public async Task UpdateCollection_FailsToUpdateCollection_WhenNotStorageCollection()
{
// Arrange
Expand Down
11 changes: 7 additions & 4 deletions src/IIIFPresentation/API.Tests/appsettings.Testing.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@
]
},
"RunMigrations": true,
"ApiSettings": {
"ResourceRoot": "https://localhost:7230",
"PageSize": 20,
"MaxPageSize": 100
"ResourceRoot": "https://localhost:7230",
"PageSize": 20,
"MaxPageSize": 100,
"AWS": {
"S3": {
"StorageBucket": "presentation-storage"
}
}
}
2 changes: 2 additions & 0 deletions src/IIIFPresentation/API/API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.SecurityToken" Version="3.7.400.25" />
<PackageReference Include="FluentValidation" Version="11.9.2" />
<PackageReference Include="iiif-net" Version="0.2.9" />
<PackageReference Include="LazyCache.AspNetCore" Version="2.4.0" />
Expand All @@ -35,6 +36,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AWS\AWS.csproj" />
<ProjectReference Include="..\Repository\Repository.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
using System.Data;
using System.Text;
using System.Data;
using API.Auth;
using API.Converters;
using API.Features.Storage.Helpers;
using API.Helpers;
using API.Infrastructure.Requests;
using API.Settings;
using AWS.S3;
using AWS.S3.Models;
using Core;
using Core.Helpers;
using MediatR;
Expand All @@ -18,19 +21,22 @@

namespace API.Features.Storage.Requests;

public class CreateCollection(int customerId, UpsertFlatCollection collection, UrlRoots urlRoots)
public class CreateCollection(int customerId, UpsertFlatCollection collection, string rawRequestBody, UrlRoots urlRoots)
: IRequest<ModifyEntityResult<PresentationCollection>>
{
public int CustomerId { get; } = customerId;

public UpsertFlatCollection Collection { get; } = collection;

public string RawRequestBody { get; } = rawRequestBody;

public UrlRoots UrlRoots { get; } = urlRoots;
}

public class CreateCollectionHandler(
PresentationContext dbContext,
ILogger<CreateCollection> logger,
IBucketWriter bucketWriter,
IIdGenerator idGenerator,
IOptions<ApiSettings> options)
: IRequestHandler<CreateCollection, ModifyEntityResult<PresentationCollection>>
Expand Down Expand Up @@ -81,7 +87,10 @@ public async Task<ModifyEntityResult<PresentationCollection>> Handle(CreateColle
Tags = request.Collection.Tags,
ItemsOrder = request.Collection.ItemsOrder
};


await using var transaction =
await dbContext.Database.BeginTransactionAsync(cancellationToken);

dbContext.Collections.Add(collection);

var saveErrors = await dbContext.TrySaveCollection(request.CustomerId, logger, cancellationToken);
Expand All @@ -91,6 +100,16 @@ public async Task<ModifyEntityResult<PresentationCollection>> Handle(CreateColle
return saveErrors;
}

if (!request.Collection.Behavior.IsStorageCollection())
{
await bucketWriter.WriteToBucket(
new ObjectInBucket(settings.AWS.S3.StorageBucket,
$"{request.CustomerId}/collections/{collection.Id}"),
request.RawRequestBody, "application/json", cancellationToken);
}

await transaction.CommitAsync(cancellationToken);

if (collection.Parent != null)
{
collection.FullPath = CollectionRetrieval.RetrieveFullPathForCollection(collection, dbContext);
Expand Down
16 changes: 12 additions & 4 deletions src/IIIFPresentation/API/Features/Storage/StorageController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@
using API.Settings;
using IIIF.Presentation;
using IIIF.Serialisation;
using Core.Helpers;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using IIIF.Presentation;

Check warning on line 17 in src/IIIFPresentation/API/Features/Storage/StorageController.cs

View workflow job for this annotation

GitHub Actions / test-dotnet

The using directive for 'IIIF.Presentation' appeared previously in this namespace
using IIIF.Serialisation;

Check warning on line 18 in src/IIIFPresentation/API/Features/Storage/StorageController.cs

View workflow job for this annotation

GitHub Actions / test-dotnet

The using directive for 'IIIF.Serialisation' appeared previously in this namespace
using Models.API.Collection.Upsert;
using Newtonsoft.Json;

namespace API.Features.Storage;

Expand Down Expand Up @@ -67,22 +71,26 @@ public async Task<IActionResult> Get(int customerId, string id, int? page = 1, i

[HttpPost("collections")]
[ETagCaching]
public async Task<IActionResult> Post(int customerId, [FromBody] UpsertFlatCollection collection,
public async Task<IActionResult> Post(int customerId,
[FromServices] UpsertFlatCollectionValidator validator)
{
if (!Request.ShowExtraProperties())
{
return this.PresentationProblem(statusCode: (int)HttpStatusCode.Forbidden);
}

var rawRequestBody = await new StreamReader(Request.Body).ReadToEndAsync();

var collection = JsonConvert.DeserializeObject<UpsertFlatCollection>(rawRequestBody);

var validation = await validator.ValidateAsync(collection);

if (!validation.IsValid)
{
return this.ValidationFailed(validation);
}

return await HandleUpsert(new CreateCollection(customerId, collection, GetUrlRoots()));
return await HandleUpsert(new CreateCollection(customerId, collection, rawRequestBody, GetUrlRoots()));
}

[HttpPut("collections/{id}")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,5 @@ public UpsertFlatCollectionValidator()
{
RuleFor(f => f.Parent).NotEmpty().WithMessage("Requires a 'parent' to be set");
RuleFor(f => f.Slug).NotEmpty().WithMessage("Requires a 'slug' to be set");

RuleFor(f => f.Behavior).Must(f => f.IsStorageCollection())
.WithMessage("'Behavior' must contain 'storage-collection' when updating");
}
}
17 changes: 17 additions & 0 deletions src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using API.Infrastructure.Mediatr.Behaviours;
using API.Infrastructure.Requests.Pipelines;
using API.Settings;
using AWS.Configuration;
using AWS.S3;
using MediatR;
using Repository;
using Sqids;
Expand Down Expand Up @@ -51,4 +53,19 @@ public static IServiceCollection ConfigureIdGenerator(this IServiceCollection se
}))
.AddSingleton<IIdGenerator, SqidsGenerator>();
}

/// <summary>
/// Add required AWS services
/// </summary>
public static IServiceCollection AddAws(this IServiceCollection services,
IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
{
services
.AddSingleton<IBucketReader, S3BucketReader>()
.AddSingleton<IBucketWriter, S3BucketWriter>()
.SetupAWS(configuration, webHostEnvironment)
.WithAmazonS3();

return services;
}
}
3 changes: 2 additions & 1 deletion src/IIIFPresentation/API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
builder.Services.AddSwaggerGen();

builder.Services.AddOptions<ApiSettings>()
.BindConfiguration(nameof(ApiSettings));
.BindConfiguration(string.Empty);
builder.Services.AddOptions<CacheSettings>()
.BindConfiguration(nameof(CacheSettings));

Expand All @@ -41,6 +41,7 @@
builder.Services.ConfigureMediatR();
builder.Services.ConfigureIdGenerator();
builder.Services.AddHealthChecks();
builder.Services.AddAws(builder.Configuration, builder.Environment);
builder.Services.Configure<ForwardedHeadersOptions>(opts =>
{
opts.ForwardedHeaders = ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto;
Expand Down
6 changes: 5 additions & 1 deletion src/IIIFPresentation/API/Settings/ApiSettings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace API.Settings;
using AWS.Settings;

namespace API.Settings;

public class ApiSettings
{
Expand All @@ -18,4 +20,6 @@ public class ApiSettings
public int MaxPageSize { get; set; } = 1000;

public string? PathBase { get; set; }

public AWSSettings AWS { get; set; }

Check warning on line 24 in src/IIIFPresentation/API/Settings/ApiSettings.cs

View workflow job for this annotation

GitHub Actions / test-dotnet

Non-nullable property 'AWS' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
}
30 changes: 30 additions & 0 deletions src/IIIFPresentation/AWS.Tests/AWS.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="3.7.403.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="FakeItEasy" Version="8.3.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Include="xunit" Version="2.5.3"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"/>
</ItemGroup>

<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AWS\AWS.csproj" />
</ItemGroup>

</Project>
Loading

0 comments on commit 0b2d019

Please sign in to comment.