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
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ object UsageRightsMetadataMapper {
ImageMetadata(byline = Some(u.creator), credit = u.publication, imageType = Some("Illustration"))
case u: Composite => ImageMetadata(credit = Some(u.suppliers), imageType = Some("Composite"))
case u: Screengrab => ImageMetadata(credit = u.source)
case u: PublisherPhotographer => ImageMetadata(byline = Some(u.creator), credit = u.publication, imageType = Some("Photograph"))
case u: PublisherIllustrator => ImageMetadata(byline = Some(u.creator), credit = u.publication, imageType = Some("Illustration"))
case u: PublisherPhotography => ImageMetadata(byline = Some(u.creator), credit = u.publication, imageType = Some("Photograph"))
case u: PublisherIllustration => ImageMetadata(byline = Some(u.creator), credit = u.publication, imageType = Some("Illustration"))
}

// if we don't match, return None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ sealed trait UsageRightsSpec {

// An optional alias for `category` that a search can also be made with -
// e.g. so the Guardian can search `category:gnm-owned-illustration` without the underlying (stable)
// category value itself needing to vary per org. See PublisherPhotographer/PublisherIllustrator.
// category value itself needing to vary per org. See PublisherPhotography/PublisherIllustration.
def searchAlias(config: CommonConfig): Option[String] = None
}
sealed trait ReplacementUsageRightsSpec extends UsageRightsSpec {
Expand Down Expand Up @@ -62,8 +62,8 @@ object UsageRightsSpec extends StrictLogging {

object UsageRights {

val photographer: NonEmptyList[UsageRightsSpec] = NonEmptyList(StaffPhotographer, ContractPhotographer, CommissionedPhotographer, PublisherPhotographer)
val illustrator: NonEmptyList[UsageRightsSpec] = NonEmptyList(StaffIllustrator, ContractIllustrator, CommissionedIllustrator, PublisherIllustrator)
val photographer: NonEmptyList[UsageRightsSpec] = NonEmptyList(StaffPhotographer, ContractPhotographer, CommissionedPhotographer, PublisherPhotography)
val illustrator: NonEmptyList[UsageRightsSpec] = NonEmptyList(StaffIllustrator, ContractIllustrator, CommissionedIllustrator, PublisherIllustration)
val whollyOwned: NonEmptyList[UsageRightsSpec] = photographer append illustrator

// this is a convenience method so that we use the same formatting for all subtypes
Expand Down Expand Up @@ -115,8 +115,8 @@ object UsageRights {
case o: ProgrammesIndependents => ProgrammesIndependents.formats.writes(o)
case o: PayToUse => PayToUse.formats.writes(o)
case o: PRAndThirdParty => PRAndThirdParty.formats.writes(o)
case o: PublisherPhotographer => PublisherPhotographer.formats.writes(o)
case o: PublisherIllustrator => PublisherIllustrator.formats.writes(o)
case o: PublisherPhotography => PublisherPhotography.formats.writes(o)
case o: PublisherIllustration => PublisherIllustration.formats.writes(o)
case o: NoRights.type => NoRights.jsonWrites.writes(o)
}

Expand Down Expand Up @@ -157,8 +157,8 @@ object UsageRights {
case ProgrammesIndependents.category => json.asOpt[ProgrammesIndependents]
case PayToUse.category => json.asOpt[PayToUse]
case PRAndThirdParty.category => json.asOpt[PRAndThirdParty]
case PublisherPhotographer.category => json.asOpt[PublisherPhotographer]
case PublisherIllustrator.category => json.asOpt[PublisherIllustrator]
case PublisherPhotography.category => json.asOpt[PublisherPhotography]
case PublisherIllustration.category => json.asOpt[PublisherIllustration]
case _ => None
})
.orElse(supplier.flatMap(_ => json.asOpt[Agency]))
Expand Down Expand Up @@ -683,13 +683,13 @@ object ProgrammesAcquisitions extends UsageRightsSpec {
UsageRights.subtypeFormat(ProgrammesAcquisitions.category)(Json.format[ProgrammesAcquisitions])
}

final case class PublisherPhotographer(creator: String, publication: Option[String] = None, restrictions: Option[String] = None) extends UsageRights {
final case class PublisherPhotography(creator: String, publication: Option[String] = None, restrictions: Option[String] = None) extends UsageRights {

override val defaultCost: Option[Cost] = PublisherPhotographer.defaultCost
override val defaultCost: Option[Cost] = PublisherPhotography.defaultCost
}

object PublisherPhotographer extends ReplacementUsageRightsSpec {
override val category: String = "publisher-photographer"
object PublisherPhotography extends ReplacementUsageRightsSpec {
override val category: String = "publisher-photography"

val legacyCategories: List[String] = List(
StaffPhotographer.category,
Expand All @@ -701,26 +701,26 @@ object PublisherPhotographer extends ReplacementUsageRightsSpec {

val allCategories: NonEmptyList[String] = NonEmptyList.fromSeq(category, legacyCategories)

override def name(config: CommonConfig): String = s"${config.staffPhotographerOrganisation} Photographer"
override def name(config: CommonConfig): String = s"${config.staffPhotographerOrganisation} Photography"

override def description(config: CommonConfig): String = s"A photograph commissioned by ${config.staffPhotographerOrganisation}, regardless of whether the contributor was staff, contract or freelance."

override def searchAlias(config: CommonConfig): Option[String] = Some(s"${config.staffPhotographerOrganisation.toLowerCase}-photographer")
override def searchAlias(config: CommonConfig): Option[String] = Some(s"${config.staffPhotographerOrganisation.toLowerCase}-photography")

override val defaultCost: Option[Cost] = Some(Free)

implicit val formats: Format[PublisherPhotographer] =
UsageRights.subtypeFormat(PublisherPhotographer.category)(Json.format[PublisherPhotographer])
implicit val formats: Format[PublisherPhotography] =
UsageRights.subtypeFormat(PublisherPhotography.category)(Json.format[PublisherPhotography])
}


final case class PublisherIllustrator(creator: String, publication: Option[String] = None, restrictions: Option[String] = None) extends UsageRights {
final case class PublisherIllustration(creator: String, publication: Option[String] = None, restrictions: Option[String] = None) extends UsageRights {

override val defaultCost: Option[Cost] = PublisherIllustrator.defaultCost
override val defaultCost: Option[Cost] = PublisherIllustration.defaultCost
}

object PublisherIllustrator extends ReplacementUsageRightsSpec {
override val category: String = "publisher-illustrator"
object PublisherIllustration extends ReplacementUsageRightsSpec {
override val category: String = "publisher-illustration"

val legacyCategories: List[String] = List(
StaffIllustrator.category,
Expand All @@ -730,14 +730,14 @@ object PublisherIllustrator extends ReplacementUsageRightsSpec {

val allCategories: NonEmptyList[String] = NonEmptyList.fromSeq(category, legacyCategories)

override def name(config: CommonConfig): String = s"${config.staffPhotographerOrganisation} Illustrator"
override def name(config: CommonConfig): String = s"${config.staffPhotographerOrganisation} Illustration"

override def description(config: CommonConfig): String = s"An illustration commissioned by ${config.staffPhotographerOrganisation}, regardless of whether the contributor was staff, contract or freelance."

override def searchAlias(config: CommonConfig): Option[String] = Some(s"${config.staffPhotographerOrganisation.toLowerCase}-illustrator")
override def searchAlias(config: CommonConfig): Option[String] = Some(s"${config.staffPhotographerOrganisation.toLowerCase}-illustration")

override val defaultCost: Option[Cost] = Some(Free)

implicit val formats: Format[PublisherIllustrator] =
UsageRights.subtypeFormat(PublisherIllustrator.category)(Json.format[PublisherIllustrator])
implicit val formats: Format[PublisherIllustration] =
UsageRights.subtypeFormat(PublisherIllustration.category)(Json.format[PublisherIllustration])
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,15 @@ class UsageRightsMetadataMapperTest extends AnyFunSpec with Matchers {
usageRightsToMetadata(ur, metadataWithoutCopyright) should be(None)
}

it ("should convert PublisherPhotographers") {
val ur = PublisherPhotographer("Alicia Canter", Some("The Guardian"))
it ("should convert PublisherPhotographys") {
val ur = PublisherPhotography("Alicia Canter", Some("The Guardian"))
usageRightsToMetadata(ur, metadataWithoutCopyright) should be (
Some(ImageMetadata(byline = Some("Alicia Canter"), credit = Some("The Guardian"), imageType = Some("Photograph")))
)
}

it ("should convert PublisherIllustrators") {
val ur = PublisherIllustrator("First Dog on the Moon Institute", Some("The Guardian"))
it ("should convert PublisherIllustrations") {
val ur = PublisherIllustration("First Dog on the Moon Institute", Some("The Guardian"))
usageRightsToMetadata(ur, metadataWithoutCopyright) should be (
Some(ImageMetadata(byline = Some("First Dog on the Moon Institute"), credit = Some("The Guardian"), imageType = Some("Illustration")))
)
Expand Down
12 changes: 6 additions & 6 deletions e2e-tests/features/rights-and-restrictions.feature
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,10 @@ Feature: Viewing and editing an image's rights and restrictions in the single im
| Agency - subscription | Supplier | required | unticked |
| Agency - subscription | Collection | optional | unticked |
| PR & Third Party | Source | required | ticked and locked |
| GNM Photographer | Publication | required | unticked |
| GNM Photographer | Creator | required | unticked |
| GNM Illustrator | Publication | required | unticked |
| GNM Illustrator | Creator | required | unticked |
| GNM Photography | Publication | required | unticked |
| GNM Photography | Creator | required | unticked |
| GNM Illustration | Publication | required | unticked |
| GNM Illustration | Creator | required | unticked |
# Evidence: metadata-editor/app/model/UsageRightsProperty.scala lines 74-77 (restrictions required when defaultCost is Conditional)
# Evidence: metadata-editor/app/model/UsageRightsProperty.scala lines 155-163 (publisher publication and creator properties)
# Evidence: common-lib/src/main/scala/com/gu/mediaservice/model/UsageRights.scala (defaultCost per category; publisher names use branding.staffPhotographerOrganisation, default "GNM")
Expand Down Expand Up @@ -234,8 +234,8 @@ Feature: Viewing and editing an image's rights and restrictions in the single im
Examples:
| category |
| Agency - subscription |
| GNM Photographer |
| GNM Illustrator |
| GNM Photography |
| GNM Illustration |

# Evidence: metadata-editor/app/model/UsageRightsProperty.scala lines 74-77 (restrictions offered for every category except NoRights)
# Evidence: metadata-editor/app/model/UsageRightsProperty.scala lines 155-163 (publisher publication and creator properties)
Expand Down
104 changes: 60 additions & 44 deletions e2e-tests/setup/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
* rewrites the LocalStack/Elasticsearch endpoints so that they resolve via the
* Testcontainers network aliases instead of the Guardian dev domains / localhost.
*/
import * as fs from 'fs';
import * as path from 'path';
import { createRequire } from 'module';
import JSON5 from 'json5';
import * as fs from "fs";
import * as path from "path";
import { createRequire } from "module";
import JSON5 from "json5";
import {
DOMAIN,
EMAIL_DOMAIN,
Expand All @@ -18,12 +18,17 @@ import {
REGION,
REPO_ROOT,
SERVICE_PORTS,
} from './constants.ts';
} from "./constants.ts";

/** `service-config.js` is CommonJS and lives outside this package, so load it via require. */
const require = createRequire(import.meta.url);

const GENERATE_CONFIG_DIR = path.join(REPO_ROOT, 'dev', 'script', 'generate-config');
const GENERATE_CONFIG_DIR = path.join(
REPO_ROOT,
"dev",
"script",
"generate-config",
);

const GRID_SERVICES = Object.keys(SERVICE_PORTS);

Expand All @@ -34,13 +39,13 @@ type StackProps = Record<string, string>;
* metadata-editor scenarios exercise real server config end-to-end rather than a
* client-side stub. Applied at stack boot, so they are present for every scenario.
*/
export const E2E_IMAGE_TYPES = ['Photograph', 'Illustration', 'Composite'];
export const E2E_IMAGE_TYPES = ["Photograph", "Illustration", "Composite"];

export const E2E_METADATA_TEMPLATE = {
templateName: 'E2E Agency',
templateName: "E2E Agency",
fields: [
{ name: 'byline', value: 'E2E Byline' },
{ name: 'credit', value: 'E2E Agency' },
{ name: "byline", value: "E2E Byline" },
{ name: "credit", value: "E2E Agency" },
],
};

Expand All @@ -51,17 +56,20 @@ export const E2E_METADATA_TEMPLATE = {
* `supplier` that Agency requires (only `suppliers`), and the metadata editor would reject it.
*/
export const E2E_RIGHTS_TEMPLATE = {
templateName: 'E2E Pay to use rights',
usageRights: { category: 'pay-to-use', restrictions: 'E2E template restriction' },
templateName: "E2E Pay to use rights",
usageRights: {
category: "pay-to-use",
restrictions: "E2E template restriction",
},
};

/**
* media-api derives `usageInstructions` from an image's usageRights category via this map, so
* an image whose category is `agency` (see the AAP-credited fixture) shows this text.
*/
export const E2E_USAGE_INSTRUCTIONS = {
category: 'agency',
text: 'These images are supplied under agency terms; check the licence before use.',
category: "agency",
text: "These images are supplied under agency terms; check the licence before use.",
};

/** Linked from the upload page for users without the upload_images permission. */
Expand All @@ -70,38 +78,41 @@ export const E2E_UPLOAD_SUPPORT_EMAIL = `mailto:grid-e2e-upload-support@${EMAIL_
/** Render the e2e-only Kahuna settings as HOCON to append to the generated `kahuna.conf`. */
function kahunaE2eConfig(): string {
const metadataFields = E2E_METADATA_TEMPLATE.fields
.map((f) => ` { name = "${f.name}", value = "${f.value}", resolveStrategy = "replace" }`)
.join('\n');
.map(
(f) =>
` { name = "${f.name}", value = "${f.value}", resolveStrategy = "replace" }`,
)
.join("\n");

return [
// The e2e stack has no Bedrock access, so AI search requests would 500.
'ai.search.enabled = false',
"ai.search.enabled = false",
`links.uploadImagesPermissionSupportEmail = "${E2E_UPLOAD_SUPPORT_EMAIL}"`,
`imageTypes = ${JSON.stringify(E2E_IMAGE_TYPES)}`,
'metadata.templates = [',
' {',
"metadata.templates = [",
" {",
` templateName = "${E2E_METADATA_TEMPLATE.templateName}"`,
' metadataFields = [',
" metadataFields = [",
metadataFields,
' ]',
' },',
' {',
" ]",
" },",
" {",
` templateName = "${E2E_RIGHTS_TEMPLATE.templateName}"`,
` usageRights = ${JSON.stringify(E2E_RIGHTS_TEMPLATE.usageRights)}`,
' }',
']',
'',
].join('\n');
" }",
"]",
"",
].join("\n");
}

/** Render the e2e-only media-api settings as HOCON to append to the generated `media-api.conf`. */
function mediaApiE2eConfig(): string {
return [
'usageInstructions {',
"usageInstructions {",
` ${E2E_USAGE_INSTRUCTIONS.category} = "${E2E_USAGE_INSTRUCTIONS.text}"`,
'}',
'',
].join('\n');
"}",
"",
].join("\n");
}

/**
Expand All @@ -112,7 +123,7 @@ function rewriteEndpoints(conf: string): string {
const localstackUrl = `http://${LOCALSTACK_ALIAS}:${LOCALSTACK_PORT}`;
// Endpoints baked in by `service-config.js` that must be redirected to the container.
const guardianLocalstackUrl = `https://${LOCALSTACK_ALIAS}.media.${DOMAIN}`;
const legacyLocalstackUrl = 'http://localhost:4576';
const legacyLocalstackUrl = "http://localhost:4576";
const localLocalstackUrl = `http://localhost:${LOCALSTACK_PORT}`;

const rewrittenConf = conf
Expand All @@ -138,9 +149,11 @@ export function generateServiceConfig(
coreStackProps: StackProps,
authStackProps: StackProps,
): void {
const ServiceConfig = require(path.join(GENERATE_CONFIG_DIR, 'service-config.js'));
const ServiceConfig = require(
path.join(GENERATE_CONFIG_DIR, "service-config.js"),
);
const defaultConfig = JSON5.parse(
fs.readFileSync(path.join(GENERATE_CONFIG_DIR, 'config.json5'), 'utf8'),
fs.readFileSync(path.join(GENERATE_CONFIG_DIR, "config.json5"), "utf8"),
);

const config = {
Expand All @@ -156,44 +169,47 @@ export function generateServiceConfig(
},
};

const serviceConfigs: Record<string, string> = ServiceConfig.getCoreConfigs(config);
const serviceConfigs: Record<string, string> =
ServiceConfig.getCoreConfigs(config);
const localAuthConfig: string = ServiceConfig.getUseLocalAuthConfig(config);

// e2e-only config appended per service on top of the generated base config.
const extraConfig: Record<string, string> = {
kahuna: kahunaE2eConfig(),
'media-api': mediaApiE2eConfig(),
"media-api": mediaApiE2eConfig(),
};

fs.mkdirSync(configDir, { recursive: true });

// Mark the stage as DEV so services load `~/.grid/<app>.conf` (see GridConfigLoader).
fs.writeFileSync(path.join(configDir, 'stage'), 'DEV');
fs.writeFileSync(path.join(configDir, "stage"), "DEV");

// The staged apps run in Play prod mode, which requires an application secret (>= 256
// bits). GridConfigLoader loads `common.conf` for every service, so set it once here
// rather than injecting it as a JVM option in the container.
fs.writeFileSync(
path.join(configDir, 'common.conf'),
path.join(configDir, "common.conf"),
[
'play.http.secret.key = "testcontainers-e2e-application-secret-0123456789"',
'enforceUploadImagesPermission = true',
"enforceUploadImagesPermission = true",
`usageRights.applicable = ${JSON.stringify([
"com.gu.mediaservice.model.NoRights",
"com.gu.mediaservice.model.PayToUse",
"com.gu.mediaservice.model.PRAndThirdParty",
"com.gu.mediaservice.model.Agency",
"com.gu.mediaservice.model.PublisherPhotographer",
"com.gu.mediaservice.model.PublisherIllustrator",
"com.gu.mediaservice.model.PublisherPhotography",
"com.gu.mediaservice.model.PublisherIllustration",
])}`,
'',
].join('\n'),
"",
].join("\n"),
);

for (const service of GRID_SERVICES) {
const conf = serviceConfigs[service];
if (!conf) {
throw new Error(`service-config.js did not produce config for '${service}'`);
throw new Error(
`service-config.js did not produce config for '${service}'`,
);
}

const rewritten = rewriteEndpoints(`${conf}\n${localAuthConfig}`);
Expand Down
Loading
Loading