Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
# Mono auto generated files
mono_crash.*

# Git Bash (MSYS2) crash dumps
*.stackdump

appsettings.Development.json
appsettings.Development*

Expand Down
9 changes: 9 additions & 0 deletions docs/notes/path-customisation.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,19 @@ Used for parsing incoming paths and id generation.

* `PathSettings:PresentationApiUrl` - default.
* `PathSettings:CustomerPresentationApiUrl:{customerId}` - customer specific override.
* `PathSettings:LegacyPresentationApiUrl` - optional legacy hostname (e.g. `presentation-api.*`, while `PresentationApiUrl`
is moving to `iiif.*`). A deployment that has never had a legacy hostname can leave this unset.
* `PathSettings:LegacyHostnameCutoffDate` - cut-off date used alongside `LegacyPresentationApiUrl`.

When generating an id, the host is chosen with the following precedence:
1. `CustomerPresentationApiUrl` override, if set for the customer.
2. `LegacyPresentationApiUrl`, if set and the resource's `Created` date is before `LegacyHostnameCutoffDate`.
3. `PresentationApiUrl`.

Related reading:
* `OrchestratorUrl` - https://github.com/dlcs/iiif-presentation/issues/367
* `PresentationApiUrl` - https://github.com/dlcs/iiif-presentation/issues/370
* `LegacyPresentationApiUrl` - https://github.com/dlcs/iiif-presentation/issues/654, [ADR 0004 - Moving `presentation-api.*` to `iiif.*`](https://github.com/dlcs/private-protagonist/blob/main/docs/adr/0004-move-presentation-url.md)

## Helpers

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,21 @@ public void GetFlatPresentationPathForRequest_ReturnsAllPaths_FromPartialOverrid
// Assert
path.Should().Be(expected);
}

[Fact]
public void GetFlatPresentationPathForRequest_IgnoresCreatedDate_AlwaysMirrorsRequestHost()
{
// Arrange - this generator always mirrors whatever host the live request came in on, so a "created" date
// (used elsewhere to choose between legacy/default hostnames) has no effect on it
var sut = new HostnameDrivenPresentationPathGenerator(Options.Create(new TypedPathTemplateOptions()),
HttpContextAccessor);

// Act
var withoutCreated = sut.GetFlatPresentationPathForRequest(PresentationResourceType.ManifestPrivate, 1, "someId");
var withCreated = sut.GetFlatPresentationPathForRequest(PresentationResourceType.ManifestPrivate, 1, "someId",
new DateTime(2000, 1, 1));

// Assert
withCreated.Should().Be(withoutCreated);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ public class HostnameDrivenPresentationPathGenerator(
{
private readonly TypedPathTemplateOptions settings = settings.Value;

public string GetHierarchyPresentationPathForRequest(string presentationServiceType, int customerId, string hierarchyPath)
public string GetHierarchyPresentationPathForRequest(string presentationServiceType, int customerId,
string? hierarchyPath, DateTime? created = null)
{
return GetPresentationPath(presentationServiceType, customerId, hierarchyPath);
}

public string GetFlatPresentationPathForRequest(string presentationServiceType, int customerId, string resourceId)
public string GetFlatPresentationPathForRequest(string presentationServiceType, int customerId, string? resourceId,
DateTime? created = null)
{
return GetPresentationPath(presentationServiceType, customerId, resourceId: resourceId);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Core.Web;
using FakeItEasy;
using Models.Database;
using Models.Database.Collections;
using Models.Database.General;
Expand All @@ -8,7 +9,10 @@

namespace Repository.Tests.Paths;

public class PathGeneratorTests
/// <summary>
/// Tests for <see cref="PathGeneratorBase"/>, exercised via <see cref="TestPathGenerator"/>.
/// </summary>
public class PathGeneratorBaseTests
{
private readonly IPathGenerator pathGenerator =
new TestPathGenerator(new TestPresentationConfigGenerator("http://base", new TypedPathTemplateOptions()));
Expand Down Expand Up @@ -62,7 +66,24 @@ public void GenerateFlatCollectionId_CreatesId()
// Assert
id.Should().Be("http://base/0/collections/test");
}


[Fact]
public void GenerateFlatCollectionId_PassesCollectionCreatedDate()
{
// Arrange
var presentationPathGenerator = A.Fake<IPresentationPathGenerator>();
var sut = new TestPathGenerator(presentationPathGenerator);
var created = new DateTime(2020, 1, 1);
var collection = new Collection { Id = "test", Created = created };

// Act
sut.GenerateFlatCollectionId(collection);

// Assert
A.CallTo(() => presentationPathGenerator.GetFlatPresentationPathForRequest(
PresentationResourceType.CollectionPrivate, 0, "test", created)).MustHaveHappened();
}

[Theory]
[InlineData(ResourceType.StorageCollection)]
[InlineData(ResourceType.IIIFCollection)]
Expand Down Expand Up @@ -121,7 +142,23 @@ public void GenerateFlatParentId_Correct(ResourceType resourceType)
// Assert
id.Should().Be("http://base/0/collections/parent");
}


[Fact]
public void GenerateFlatParentId_PassesNullCreatedDate()
{
// Arrange
var presentationPathGenerator = A.Fake<IPresentationPathGenerator>();
var sut = new TestPathGenerator(presentationPathGenerator);
var hierarchy = new Hierarchy { Slug = "test", Parent = "parent" };

// Act
sut.GenerateFlatParentId(hierarchy);

// Assert
A.CallTo(() => presentationPathGenerator.GetFlatPresentationPathForRequest(
PresentationResourceType.CollectionPrivate, 0, "parent", null)).MustHaveHappened();
}

[Fact]
public void GenerateFlatCollectionViewId_CreatesViewId()
{
Expand Down Expand Up @@ -307,7 +344,24 @@ public void GenerateFlatManifestId_Correct()
// Assert
id.Should().Be("http://base/123/manifests/test");
}


[Fact]
public void GenerateFlatManifestId_PassesManifestCreatedDate()
{
// Arrange
var presentationPathGenerator = A.Fake<IPresentationPathGenerator>();
var sut = new TestPathGenerator(presentationPathGenerator);
var created = new DateTime(2020, 1, 1);
var manifest = new Manifest { Id = "test", CustomerId = 1, Created = created };

// Act
sut.GenerateFlatManifestId(manifest);

// Assert
A.CallTo(() => presentationPathGenerator.GetFlatPresentationPathForRequest(
PresentationResourceType.ManifestPrivate, 1, "test", created)).MustHaveHappened();
}

[Fact]
public void GenerateCanvasId_Correct()
{
Expand All @@ -324,7 +378,24 @@ public void GenerateCanvasId_Correct()
// Assert
id.Should().Be("http://base/123/canvases/test");
}


[Fact]
public void GenerateCanvasId_PassesCanvasPaintingCreatedDate()
{
// Arrange
var presentationPathGenerator = A.Fake<IPresentationPathGenerator>();
var sut = new TestPathGenerator(presentationPathGenerator);
var created = new DateTime(2020, 1, 1);
var canvasPainting = new CanvasPainting { Id = "test", CustomerId = 1, Created = created };

// Act
sut.GenerateCanvasId(canvasPainting);

// Assert
A.CallTo(() => presentationPathGenerator.GetFlatPresentationPathForRequest(
PresentationResourceType.Canvas, 1, "test", created)).MustHaveHappened();
}

[Theory]
[InlineData(null)]
[InlineData("")]
Expand Down
2 changes: 1 addition & 1 deletion src/IIIFPresentation/Repository/Paths/IPathGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ string GenerateFlatCollectionSearchView(Collection collection, string label, int
Uri? GenerateAssetUri(CanvasPainting canvasPainting);

/// <summary>
/// Generate the hierarchical id for specified customer and path slugs
/// Generate the hierarchical id for specified customer and path slugs
/// </summary>
string GenerateHierarchicalFromFullPath(int customerId, string? fullPath);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@ public interface IPresentationPathGenerator
/// <summary>
/// Generate full path for IIIF Hierarchical Presentation resources
/// </summary>
/// <param name="created">
/// Created date of the resource the path is being generated for, left `null` if not known yet
/// </param>
public string GetHierarchyPresentationPathForRequest(string presentationServiceType, int customerId,
string hierarchyPath);
string? hierarchyPath, DateTime? created = null);

/// <summary>
/// Generate full path for IIIF Presentation resources
/// </summary>
public string GetFlatPresentationPathForRequest(string presentationServiceType, int customerId, string resourceId);
/// <param name="created">
/// Created date of the resource the path is being generated for, left `null` if not known yet
/// </param>
public string GetFlatPresentationPathForRequest(string presentationServiceType, int customerId, string? resourceId,
DateTime? created = null);
}
24 changes: 12 additions & 12 deletions src/IIIFPresentation/Repository/Paths/PathGeneratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,21 @@ public abstract class PathGeneratorBase(IPresentationPathGenerator presentationP
protected abstract Uri DlcsApiUrl { get; }

public string GenerateHierarchicalFromFullPath(int customerId, string? fullPath) =>
Comment thread
JackLewis-digirati marked this conversation as resolved.
presentationPathGenerator.GetHierarchyPresentationPathForRequest(PresentationResourceType.ResourcePublic,
presentationPathGenerator.GetHierarchyPresentationPathForRequest(PresentationResourceType.ResourcePublic,
customerId, fullPath);

public string GenerateFlatCollectionId(Collection collection) =>
presentationPathGenerator.GetFlatPresentationPathForRequest(PresentationResourceType.CollectionPrivate,
collection.CustomerId, collection.Id);
collection.CustomerId, collection.Id, collection.Created);

public string GenerateHierarchicalId(Hierarchy hierarchy) =>
presentationPathGenerator.GetHierarchyPresentationPathForRequest(PresentationResourceType.ResourcePublic,
presentationPathGenerator.GetHierarchyPresentationPathForRequest(PresentationResourceType.ResourcePublic,
hierarchy.CustomerId, hierarchy.FullPath);

public string GenerateFlatId(Hierarchy hierarchy) =>
presentationPathGenerator.GetFlatPresentationPathForRequest(GetResourceType(hierarchy.Type),
presentationPathGenerator.GetFlatPresentationPathForRequest(GetResourceType(hierarchy.Type),
hierarchy.CustomerId, hierarchy.ResourceId);

public string GenerateFlatParentId(Hierarchy hierarchy) =>
presentationPathGenerator.GetFlatPresentationPathForRequest(PresentationResourceType.CollectionPrivate,
hierarchy.CustomerId,
Expand Down Expand Up @@ -73,12 +73,12 @@ public string GenerateFullPath(Hierarchy hierarchy, string? parentPath)
=> $"{(!string.IsNullOrEmpty(parentPath) ? $"{parentPath}/" : string.Empty)}{hierarchy.Slug}";

public string GenerateFlatManifestId(Manifest manifest) =>
presentationPathGenerator.GetFlatPresentationPathForRequest(PresentationResourceType.ManifestPrivate,
manifest.CustomerId, manifest.Id);
presentationPathGenerator.GetFlatPresentationPathForRequest(PresentationResourceType.ManifestPrivate,
manifest.CustomerId, manifest.Id, manifest.Created);

public string GenerateCanvasId(CanvasPainting canvasPainting) =>
presentationPathGenerator.GetFlatPresentationPathForRequest(PresentationResourceType.Canvas,
canvasPainting.CustomerId, canvasPainting.Id);
public string GenerateCanvasId(CanvasPainting canvasPainting) =>
presentationPathGenerator.GetFlatPresentationPathForRequest(PresentationResourceType.Canvas,
canvasPainting.CustomerId, canvasPainting.Id, canvasPainting.Created);

public string GenerateCanvasIdWithTarget(CanvasPainting canvasPainting)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using Microsoft.Extensions.Options;
using Repository.Paths;
using Services.Manifests.Helpers;
using Services.Manifests.Settings;

namespace Services.Tests.Manifests.Helpers;

public class SettingsDrivenPresentationConfigGeneratorTests
{
private static readonly DateTime CutoffDate = new(2026, 1, 1);

private static SettingsDrivenPresentationConfigGenerator GetSut(bool withLegacy = true, bool withCustomer = true) =>
new(Options.Create(new PathSettings
{
PresentationApiUrl = new Uri("https://iiif.example.com"),
LegacyPresentationApiUrl = withLegacy ? new Uri("https://presentation-api.example.com") : null,
LegacyHostnameCutoffDate = withLegacy ? CutoffDate : null,
CustomerPresentationApiUrl = withCustomer
? new Dictionary<int, Uri> { [1] = new Uri("https://customer.example.com") }
: new Dictionary<int, Uri>()
}));

[Fact]
public void GetFlatPresentationPathForRequest_UsesLegacyHost_IfCreatedBeforeCutoff()
{
var sut = GetSut();

var path = sut.GetFlatPresentationPathForRequest(PresentationResourceType.ManifestPrivate, 999, "foo",
CutoffDate.AddDays(-1));

path.Should().Be("https://presentation-api.example.com/999/manifests/foo");
}

[Fact]
public void GetFlatPresentationPathForRequest_UsesDefaultHost_IfCreatedOnOrAfterCutoff()
{
var sut = GetSut();

var path = sut.GetFlatPresentationPathForRequest(PresentationResourceType.ManifestPrivate, 999, "foo",
CutoffDate);

path.Should().Be("https://iiif.example.com/999/manifests/foo");
}

[Fact]
public void GetFlatPresentationPathForRequest_UsesDefaultHost_IfCreatedNotProvided()
{
var sut = GetSut();

var path = sut.GetFlatPresentationPathForRequest(PresentationResourceType.ManifestPrivate, 999, "foo");

path.Should().Be("https://iiif.example.com/999/manifests/foo");
}

[Fact]
public void GetFlatPresentationPathForRequest_UsesCustomerHost_RegardlessOfCreatedDate()
{
var sut = GetSut();

var path = sut.GetFlatPresentationPathForRequest(PresentationResourceType.ManifestPrivate, 1, "foo",
CutoffDate.AddDays(-1));

path.Should().Be("https://customer.example.com/1/manifests/foo");
}

[Fact]
public void GetHierarchyPresentationPathForRequest_UsesLegacyHost_IfCreatedBeforeCutoff()
{
var sut = GetSut();

var path = sut.GetHierarchyPresentationPathForRequest(PresentationResourceType.ResourcePublic, 999,
"some/path", CutoffDate.AddYears(-1));

path.Should().Be("https://presentation-api.example.com/999/some/path");
}

[Fact]
public void GetFlatPresentationPathForRequest_UsesDefaultHost_IfLegacyNotConfigured()
{
var sut = GetSut(withLegacy: false);

var path = sut.GetFlatPresentationPathForRequest(PresentationResourceType.ManifestPrivate, 999, "foo",
CutoffDate.AddYears(-10));

path.Should().Be("https://iiif.example.com/999/manifests/foo");
}
}
Loading
Loading