Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: validate scene thumbnail #201

Merged
merged 5 commits into from
May 15, 2023
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
48 changes: 32 additions & 16 deletions src/validations/scene.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,43 @@
import { EntityType } from '@dcl/schemas'
import { DeploymentToValidate, OK, ValidateFn, validationFailed, ValidationResponse } from '../types'
import { validateAfterADR173, validateIfTypeMatches } from './validations'

function validateIfScene(validateFn: ValidateFn): ValidateFn {
return validateIfTypeMatches(EntityType.SCENE, validateFn)
}
import { ContentMapping, EntityType } from '@dcl/schemas'
import { DeploymentToValidate, OK, validationFailed, ValidationResponse } from '../types'
import { validateAfterADR173, validateAfterADR236, validateAll, validateIfTypeMatches } from './validations'

/**
* Validate that given scene deployment does not contain worldConfiguration section
* @public
*/
export const noWorldsConfigurationValidateFn = validateIfScene(
validateAfterADR173(async function validateFn(deployment: DeploymentToValidate): Promise<ValidationResponse> {
const sceneHasWorldConfiguration = deployment.entity.metadata?.worldConfiguration !== undefined
if (sceneHasWorldConfiguration) {
return validationFailed('Scenes cannot have worldConfiguration section after ADR 173.')
export const noWorldsConfigurationValidateFn = validateAfterADR173(async function validateFn(
deployment: DeploymentToValidate
): Promise<ValidationResponse> {
const sceneHasWorldConfiguration = deployment.entity.metadata?.worldConfiguration !== undefined
if (sceneHasWorldConfiguration) {
return validationFailed('Scenes cannot have worldConfiguration section after ADR 173.')
}
return OK
})

/**
* Validate that given scene deployment thumbnail is a file embedded in the deployment
* @public
*/
export const embeddedThumbnail = validateAfterADR236(async function validateFn(
deployment: DeploymentToValidate
): Promise<ValidationResponse> {
const sceneThumbnail = deployment.entity.metadata?.display.navmapThumbnail
if (sceneThumbnail) {
const isFilePresent = deployment.entity.content.some((content: ContentMapping) => content.file === sceneThumbnail)
if (!isFilePresent) {
return validationFailed(`Scene thumbnail '${sceneThumbnail}' must be a file included in the deployment.`)
}
return OK
})
)
}
return OK
})

/**
* Validate that given scene deployment is valid
* @public
*/
export const sceneValidateFn = noWorldsConfigurationValidateFn
export const sceneValidateFn = validateIfTypeMatches(
EntityType.SCENE,
validateAll(noWorldsConfigurationValidateFn, embeddedThumbnail)
)
6 changes: 6 additions & 0 deletions src/validations/timestamps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export const ADR_158_TIMESTAMP = process.env.ADR_158_TIMESTAMP ? parseInt(proces
*/
export const ADR_173_TIMESTAMP = process.env.ADR_173_TIMESTAMP ? parseInt(process.env.ADR_173_TIMESTAMP) : 1673967600000

/**
* 1681660800000 = 2023-05-19T12:00:00Z
* @public
*/
export const ADR_236_TIMESTAMP = process.env.ADR_236_TIMESTAMP ? parseInt(process.env.ADR_236_TIMESTAMP) : 1681660800000

/**
* DCL Launch Day
* @public
Expand Down
5 changes: 5 additions & 0 deletions src/validations/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DeploymentToValidate, OK, ValidateFn } from '../types'
import {
ADR_158_TIMESTAMP,
ADR_173_TIMESTAMP,
ADR_236_TIMESTAMP,
ADR_45_TIMESTAMP,
ADR_74_TIMESTAMP,
ADR_75_TIMESTAMP
Expand Down Expand Up @@ -53,6 +54,10 @@ export function validateAfterADR173(validate: ValidateFn): ValidateFn {
return validateIfConditionMet((deployment) => deployment.entity.timestamp >= ADR_173_TIMESTAMP, validate)
}

export function validateAfterADR236(validate: ValidateFn): ValidateFn {
return validateIfConditionMet((deployment) => deployment.entity.timestamp >= ADR_236_TIMESTAMP, validate)
}

export function validateIfTypeMatches(entityType: EntityType, validate: ValidateFn) {
return validateIfConditionMet((deployment) => deployment.entity.type === entityType, validate)
}
16 changes: 16 additions & 0 deletions test/setup/mock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ILoggerComponent } from '@well-known-components/interfaces'
import { ISubgraphComponent, Variables } from '@well-known-components/thegraph-component'
import { ContentValidatorComponents, ExternalCalls, ValidateFn } from '../../src/types'
import sharp from 'sharp'

export type QueryGraph = <T = any>(query: string, variables?: Variables, remainingAttempts?: number) => Promise<T>

Expand Down Expand Up @@ -40,3 +41,18 @@ export function buildExternalCalls(externalCalls?: Partial<ExternalCalls>): Exte
export const createMockSubgraphComponent = (mock?: QueryGraph): ISubgraphComponent => ({
query: mock ?? (jest.fn() as jest.MockedFunction<QueryGraph>)
})

export const createImage = async (size: number, format: 'png' | 'jpg' = 'png'): Promise<Buffer> => {
let image = sharp({
create: {
width: size,
height: size,
channels: 4,
background: { r: 255, g: 0, b: 0, alpha: 0.5 }
}
})
if (format) {
image = format === 'png' ? image.png() : image.jpeg()
}
return await image.toBuffer()
}
17 changes: 1 addition & 16 deletions test/unit/validations/items/emotes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ADR_74_TIMESTAMP } from '../../../../src/validations/timestamps'
import { buildDeployment } from '../../../setup/deployments'
import { VALID_STANDARD_EMOTE_METADATA } from '../../../setup/emotes'
import { buildEntity } from '../../../setup/entity'
import { buildComponents, buildExternalCalls } from '../../../setup/mock'
import { buildComponents, buildExternalCalls, createImage } from '../../../setup/mock'

describe('Emotes', () => {
const timestampAfterADR74 = ADR_74_TIMESTAMP + 1
Expand All @@ -24,21 +24,6 @@ describe('Emotes', () => {
const fileName = 'thumbnail.png'
const hash = 'thumbnail'

const createImage = async (size: number, format: 'png' | 'jpg' = 'png'): Promise<Buffer> => {
let image = sharp({
create: {
width: size,
height: size,
channels: 4,
background: { r: 255, g: 0, b: 0, alpha: 0.5 }
}
})
if (format) {
image = format === 'png' ? image.png() : image.jpeg()
}
return await image.toBuffer()
}

beforeAll(async () => {
validThumbnailBuffer = await createImage(1024)
invalidThumbnailBuffer = await createImage(1025)
Expand Down
17 changes: 1 addition & 16 deletions test/unit/validations/items/wearables.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { createSizeValidateFn } from '../../../../src/validations/size'
import { ADR_45_TIMESTAMP } from '../../../../src/validations/timestamps'
import { buildDeployment } from '../../../setup/deployments'
import { buildEntity } from '../../../setup/entity'
import { buildComponents, buildExternalCalls } from '../../../setup/mock'
import { buildComponents, buildExternalCalls, createImage } from '../../../setup/mock'
import { VALID_WEARABLE_METADATA } from '../../../setup/wearable'

describe('Wearables', () => {
Expand All @@ -22,21 +22,6 @@ describe('Wearables', () => {
const fileName = 'thumbnail.png'
const hash = 'thumbnail'

const createImage = async (size: number, format: 'png' | 'jpg' = 'png'): Promise<Buffer> => {
let image = sharp({
create: {
width: size,
height: size,
channels: 4,
background: { r: 255, g: 0, b: 0, alpha: 0.5 }
}
})
if (format) {
image = format === 'png' ? image.png() : image.jpeg()
}
return await image.toBuffer()
}

beforeAll(async () => {
validThumbnailBuffer = await createImage(1024)
invalidThumbnailBuffer = await createImage(1025)
Expand Down
17 changes: 1 addition & 16 deletions test/unit/validations/profiles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { ADR_45_TIMESTAMP, ADR_74_TIMESTAMP, ADR_75_TIMESTAMP } from '../../../src/validations/timestamps'
import { buildDeployment } from '../../setup/deployments'
import { buildEntity } from '../../setup/entity'
import { buildComponents, buildExternalCalls } from '../../setup/mock'
import { buildComponents, buildExternalCalls, createImage } from '../../setup/mock'
import { validProfileMetadataWithEmotes, VALID_PROFILE_METADATA } from '../../setup/profiles'

describe('Profiles', () => {
Expand All @@ -26,21 +26,6 @@ describe('Profiles', () => {
const fileName = 'face256.png'
const hash = 'bafybeiasb5vpmaounyilfuxbd3lryvosl4yefqrfahsb2esg46q6tu6y5s'

const createImage = async (size: number, format: 'png' | 'jpg' = 'png'): Promise<Buffer> => {
let image = sharp({
create: {
width: size,
height: size,
channels: 4,
background: { r: 255, g: 0, b: 0, alpha: 0.5 }
}
})
if (format) {
image = format === 'png' ? image.png() : image.jpeg()
}
return await image.toBuffer()
}

beforeAll(async () => {
validThumbnailBuffer = await createImage(256)
invalidThumbnailBuffer = await createImage(1)
Expand Down
61 changes: 59 additions & 2 deletions test/unit/validations/scenes.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { EntityType } from '@dcl/schemas'
import { ValidationResponse } from '../../../src'
import { noWorldsConfigurationValidateFn } from '../../../src/validations/scene'
import { ADR_173_TIMESTAMP } from '../../../src/validations/timestamps'
import { embeddedThumbnail, noWorldsConfigurationValidateFn } from '../../../src/validations/scene'
import { ADR_173_TIMESTAMP, ADR_236_TIMESTAMP } from '../../../src/validations/timestamps'
import { buildDeployment } from '../../setup/deployments'
import { buildEntity } from '../../setup/entity'
import { VALID_SCENE_METADATA } from '../../setup/scenes'
import { createImage } from '../../setup/mock'

describe('Scenes', () => {
const timestamp = ADR_173_TIMESTAMP + 1
Expand Down Expand Up @@ -43,4 +44,60 @@ describe('Scenes', () => {
expect(result.errors).toContain('Scenes cannot have worldConfiguration section after ADR 173.')
})
})

describe('embeddedThumbnail validation:', () => {
const timestamp = ADR_236_TIMESTAMP + 1

let thumbnailBuffer: Buffer
const content = [{ file: 'thumbnail.png', hash: 'thumbnailHash' }]
const files = new Map()

beforeAll(async () => {
thumbnailBuffer = await createImage(1024)
files.set('thumbnailHash', thumbnailBuffer)
})

it('When there is a thumbnail that references an embedded file, validation succeeds', async () => {
const entity = buildEntity({
type: EntityType.SCENE,
metadata: {
...VALID_SCENE_METADATA,
display: {
...VALID_SCENE_METADATA.display,
navmapThumbnail: 'thumbnail.png'
}
},
content,
timestamp
})
const deployment = buildDeployment({ entity, files })

const result: ValidationResponse = await embeddedThumbnail(deployment)

expect(result.ok).toBeTruthy()
})

it('When there is a thumbnail that does not reference an embedded file, validation fails with error', async () => {
const entity = buildEntity({
type: EntityType.SCENE,
metadata: {
...VALID_SCENE_METADATA,
display: {
...VALID_SCENE_METADATA.display,
navmapThumbnail: 'https://example.com/image.png' // Invalid, it must reference a file in the deployment
}
},
content,
timestamp
})
const deployment = buildDeployment({ entity, files })

const result: ValidationResponse = await embeddedThumbnail(deployment)

expect(result.ok).toBeFalsy()
expect(result.errors).toContain(
"Scene thumbnail 'https://example.com/image.png' must be a file included in the deployment."
)
})
})
})
Loading