From e1f54320c56c5865402caa3c2f099ad91ed6c444 Mon Sep 17 00:00:00 2001 From: Kadi Kraman Date: Wed, 27 Sep 2023 11:16:00 +0100 Subject: [PATCH] [schemer] [ENG-9956]: validate for unsupported images (#4764) * Add test for invlid app icon path * Convert tests to ts and tidy assertions * Ensure schema errors for local webp images * Ensure tests check for errors * Validate for file extension mismatch * Fix test for remote image * Add changelog entry --- CHANGELOG.md | 1 + .../__snapshots__/network-test.ts.snap | 21 ++ .../__tests__/__snapshots__/test.js.snap | 77 ----- .../__tests__/__snapshots__/test.ts.snap | 196 ++++++++++++ .../__tests__/files/invalidAppIcon.json | 10 + .../schemer/__tests__/files/secretlyPng.jpg | Bin 0 -> 5970 bytes packages/schemer/__tests__/files/webp.webp | Bin 0 -> 10474 bytes .../{helper-test.js => helper-test.ts} | 0 .../__tests__/{network.js => network-test.ts} | 24 +- packages/schemer/__tests__/test.js | 155 ---------- packages/schemer/__tests__/test.ts | 291 ++++++++++++++++++ packages/schemer/package.json | 5 +- packages/schemer/src/Error.ts | 1 + packages/schemer/src/index.ts | 26 +- yarn.lock | 19 +- 15 files changed, 560 insertions(+), 266 deletions(-) create mode 100644 packages/schemer/__tests__/__snapshots__/network-test.ts.snap delete mode 100644 packages/schemer/__tests__/__snapshots__/test.js.snap create mode 100644 packages/schemer/__tests__/__snapshots__/test.ts.snap create mode 100644 packages/schemer/__tests__/files/invalidAppIcon.json create mode 100644 packages/schemer/__tests__/files/secretlyPng.jpg create mode 100644 packages/schemer/__tests__/files/webp.webp rename packages/schemer/__tests__/{helper-test.js => helper-test.ts} (100%) rename packages/schemer/__tests__/{network.js => network-test.ts} (61%) delete mode 100644 packages/schemer/__tests__/test.js create mode 100644 packages/schemer/__tests__/test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 86c1617371..e3099f7886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ This is the log of notable changes to Expo CLI and related packages. - [create-expo] Bump @expo/package-manager for Bun support - [create-expo] detect bun package manager ([#4752](https://github.com/expo/expo-cli/issues/4752)) - [webpack]: Bump expo to SDK 49 ([#4747](https://github.com/expo/expo-cli/issues/4747)) +- [schemer]: additional validation for unsupported image formats ([#4764](https://github.com/expo/expo-cli/pull/4764)) ### 🧹 Chores diff --git a/packages/schemer/__tests__/__snapshots__/network-test.ts.snap b/packages/schemer/__tests__/__snapshots__/network-test.ts.snap new file mode 100644 index 0000000000..633dc11d0f --- /dev/null +++ b/packages/schemer/__tests__/__snapshots__/network-test.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Remote Remote icon dimensions wrong 1`] = ` +Array [ + Object { + "data": "https://httpbin.org/image/png", + "errorCode": "INVALID_DIMENSIONS", + "fieldPath": "icon", + "message": "'icon' should have dimensions 101x100, but the file at 'https://httpbin.org/image/png' has dimensions 100x100", + "meta": Object { + "asset": true, + "contentTypePattern": "^image/png$", + "dimensions": Object { + "height": 100, + "width": 101, + }, + }, + "name": "ValidationError", + }, +] +`; diff --git a/packages/schemer/__tests__/__snapshots__/test.js.snap b/packages/schemer/__tests__/__snapshots__/test.js.snap deleted file mode 100644 index 8dd6c9b1f7..0000000000 --- a/packages/schemer/__tests__/__snapshots__/test.js.snap +++ /dev/null @@ -1,77 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Holistic Unit Test bad example app.json schema 1`] = ` -Array [ - Object { - "data": Object { - "asdfasdfandroid": Object { - "package": "com.yourcompany.yourappname", - }, - "icon": "DoesNotExist.png", - "orientaasdfasdftion": "portrait", - "sdkVersion": "17.0.0abad", - "slug": "1*@)#($*@)(#$*)", - }, - "errorCode": "SCHEMA_MISSING_REQUIRED_PROPERTY", - "fieldPath": "", - "message": "is missing required property 'name'", - "meta": undefined, - "name": "ValidationError", - }, - Object { - "data": Object { - "asdfasdfandroid": Object { - "package": "com.yourcompany.yourappname", - }, - "icon": "DoesNotExist.png", - "orientaasdfasdftion": "portrait", - "sdkVersion": "17.0.0abad", - "slug": "1*@)#($*@)(#$*)", - }, - "errorCode": "SCHEMA_ADDITIONAL_PROPERTY", - "fieldPath": "", - "message": "should NOT have additional property 'orientaasdfasdftion'", - "meta": undefined, - "name": "ValidationError", - }, - Object { - "data": Object { - "asdfasdfandroid": Object { - "package": "com.yourcompany.yourappname", - }, - "icon": "DoesNotExist.png", - "orientaasdfasdftion": "portrait", - "sdkVersion": "17.0.0abad", - "slug": "1*@)#($*@)(#$*)", - }, - "errorCode": "SCHEMA_ADDITIONAL_PROPERTY", - "fieldPath": "", - "message": "should NOT have additional property 'asdfasdfandroid'", - "meta": undefined, - "name": "ValidationError", - }, - Object { - "data": "1*@)#($*@)(#$*)", - "errorCode": "SCHEMA_INVALID_PATTERN", - "fieldPath": "slug", - "message": "'slug' must match pattern \\"^[a-zA-Z0-9_\\\\-]+$\\"", - "meta": undefined, - "name": "ValidationError", - }, -] -`; - -exports[`Holistic Unit Test bad example app.json schema with field with not 1`] = ` -Array [ - Object { - "data": "1.0", - "errorCode": "SCHEMA_INVALID_NOT", - "fieldPath": "runtimeVersion", - "message": "'runtimeVersion' should be not a decimal ending in a 0.", - "meta": Object { - "notHuman": "Not a decimal ending in a 0.", - }, - "name": "ValidationError", - }, -] -`; diff --git a/packages/schemer/__tests__/__snapshots__/test.ts.snap b/packages/schemer/__tests__/__snapshots__/test.ts.snap new file mode 100644 index 0000000000..601b840775 --- /dev/null +++ b/packages/schemer/__tests__/__snapshots__/test.ts.snap @@ -0,0 +1,196 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Holistic Unit Test bad example app.json - invalid path for app icon 1`] = ` +Array [ + Object { + "data": "./unknown/path.png", + "errorCode": "INVALID_ASSET_URI", + "fieldPath": "icon", + "message": "cannot access file at './unknown/path.png'", + "meta": Object { + "asset": true, + "bareWorkflow": "To change your app's icon, edit or replace the files in \`ios//Assets.xcassets/AppIcon.appiconset\` (we recommend using Xcode), and \`android/app/src/main/res/mipmap-\`. Be sure to follow the guidelines for each platform ([iOS](https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/app-icon/), [Android 7.1 and below](https://material.io/design/iconography/#icon-treatments), and [Android 8+](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)) and to provide your new icon in each existing size.", + "contentTypeHuman": ".png image", + "contentTypePattern": "^image/png$", + "square": true, + }, + "name": "ValidationError", + }, +] +`; + +exports[`Holistic Unit Test bad example app.json schema 1`] = ` +Array [ + Object { + "data": Object { + "asdfasdfandroid": Object { + "package": "com.yourcompany.yourappname", + }, + "icon": "DoesNotExist.png", + "orientaasdfasdftion": "portrait", + "sdkVersion": "17.0.0abad", + "slug": "1*@)#($*@)(#$*)", + }, + "errorCode": "SCHEMA_MISSING_REQUIRED_PROPERTY", + "fieldPath": "", + "message": "is missing required property 'name'", + "meta": undefined, + "name": "ValidationError", + }, + Object { + "data": Object { + "asdfasdfandroid": Object { + "package": "com.yourcompany.yourappname", + }, + "icon": "DoesNotExist.png", + "orientaasdfasdftion": "portrait", + "sdkVersion": "17.0.0abad", + "slug": "1*@)#($*@)(#$*)", + }, + "errorCode": "SCHEMA_ADDITIONAL_PROPERTY", + "fieldPath": "", + "message": "should NOT have additional property 'orientaasdfasdftion'", + "meta": undefined, + "name": "ValidationError", + }, + Object { + "data": Object { + "asdfasdfandroid": Object { + "package": "com.yourcompany.yourappname", + }, + "icon": "DoesNotExist.png", + "orientaasdfasdftion": "portrait", + "sdkVersion": "17.0.0abad", + "slug": "1*@)#($*@)(#$*)", + }, + "errorCode": "SCHEMA_ADDITIONAL_PROPERTY", + "fieldPath": "", + "message": "should NOT have additional property 'asdfasdfandroid'", + "meta": undefined, + "name": "ValidationError", + }, + Object { + "data": "1*@)#($*@)(#$*)", + "errorCode": "SCHEMA_INVALID_PATTERN", + "fieldPath": "slug", + "message": "'slug' must match pattern \\"^[a-zA-Z0-9_\\\\-]+$\\"", + "meta": undefined, + "name": "ValidationError", + }, +] +`; + +exports[`Holistic Unit Test bad example app.json schema with field with not 1`] = ` +Array [ + Object { + "data": "1.0", + "errorCode": "SCHEMA_INVALID_NOT", + "fieldPath": "runtimeVersion", + "message": "'runtimeVersion' should be not a decimal ending in a 0.", + "meta": Object { + "notHuman": "Not a decimal ending in a 0.", + }, + "name": "ValidationError", + }, +] +`; + +exports[`Image Validation errors for webp images 1`] = ` +Array [ + Object { + "data": "./files/webp.webp", + "errorCode": "INVALID_CONTENT_TYPE", + "fieldPath": "Android.adaptiveIcon.foregroundImage", + "message": "field 'Android.adaptiveIcon.foregroundImage' should point to .png image but the file at './files/webp.webp' has type webp", + "meta": Object { + "asset": true, + "contentTypeHuman": ".png image", + "contentTypePattern": "^image/png$", + "square": true, + }, + "name": "ValidationError", + }, + Object { + "data": "./files/webp.webp", + "errorCode": "NOT_SQUARE", + "fieldPath": "Android.adaptiveIcon.foregroundImage", + "message": "image should be square, but the file at './files/webp.webp' has dimensions 320x214", + "meta": Object { + "asset": true, + "contentTypeHuman": ".png image", + "contentTypePattern": "^image/png$", + "square": true, + }, + "name": "ValidationError", + }, +] +`; + +exports[`Image Validation errors when file extension and content do not match up 1`] = ` +Array [ + Object { + "data": "./files/secretlyPng.jpg", + "errorCode": "FILE_EXTENSION_MISMATCH", + "fieldPath": "icon", + "message": "the file extension should match the content, but the file extension is .jpg while the file content at './files/secretlyPng.jpg' is of type png", + "meta": Object { + "asset": true, + "bareWorkflow": "To change your app's icon, edit or replace the files in \`ios//Assets.xcassets/AppIcon.appiconset\` (we recommend using Xcode), and \`android/app/src/main/res/mipmap-\`. Be sure to follow the guidelines for each platform ([iOS](https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/app-icon/), [Android 7.1 and below](https://material.io/design/iconography/#icon-treatments), and [Android 8+](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)) and to provide your new icon in each existing size.", + "contentTypeHuman": ".png image", + "contentTypePattern": "^image/png$", + "square": true, + }, + "name": "ValidationError", + }, +] +`; + +exports[`Individual Unit Tests Error when data has an additional property 1`] = ` +Array [ + Object { + "data": Object { + "extraProperty": "extra", + }, + "errorCode": "SCHEMA_ADDITIONAL_PROPERTY", + "fieldPath": "", + "message": "should NOT have additional property 'extraProperty'", + "meta": undefined, + "name": "ValidationError", + }, +] +`; + +exports[`Individual Unit Tests Error when missing Required Property 1`] = ` +Array [ + Object { + "data": Object { + "noName": "", + }, + "errorCode": "SCHEMA_MISSING_REQUIRED_PROPERTY", + "fieldPath": "", + "message": "is missing required property 'name'", + "meta": undefined, + "name": "ValidationError", + }, +] +`; + +exports[`Manual Validation Individual Unit Tests Local icon dimensions wrong 1`] = ` +Array [ + Object { + "data": "./files/check.png", + "errorCode": "INVALID_DIMENSIONS", + "fieldPath": "icon", + "message": "'icon' should have dimensions 400x401, but the file at './files/check.png' has dimensions 512x512", + "meta": Object { + "asset": true, + "contentTypePattern": "^image/png$", + "dimensions": Object { + "height": 401, + "width": 400, + }, + }, + "name": "ValidationError", + }, +] +`; diff --git a/packages/schemer/__tests__/files/invalidAppIcon.json b/packages/schemer/__tests__/files/invalidAppIcon.json new file mode 100644 index 0000000000..8067378062 --- /dev/null +++ b/packages/schemer/__tests__/files/invalidAppIcon.json @@ -0,0 +1,10 @@ +{ + "name": "test app", + "slug": "asdfasdf", + "sdkVersion": "17.0.0", + "orientation": "portrait", + "android": { + "package": "com.yourcompany.yourappname" + }, + "icon": "./unknown/path.png" +} diff --git a/packages/schemer/__tests__/files/secretlyPng.jpg b/packages/schemer/__tests__/files/secretlyPng.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9c34ad30b1c1d9581262b89882d12f6ad9391a73 GIT binary patch literal 5970 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 literal 0 HcmV?d00001 diff --git a/packages/schemer/__tests__/files/webp.webp b/packages/schemer/__tests__/files/webp.webp new file mode 100644 index 0000000000000000000000000000000000000000..0da983e2ce5335eb13a7b793a1403588617dde3c GIT binary patch literal 10474 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 literal 0 HcmV?d00001 diff --git a/packages/schemer/__tests__/helper-test.js b/packages/schemer/__tests__/helper-test.ts similarity index 100% rename from packages/schemer/__tests__/helper-test.js rename to packages/schemer/__tests__/helper-test.ts diff --git a/packages/schemer/__tests__/network.js b/packages/schemer/__tests__/network-test.ts similarity index 61% rename from packages/schemer/__tests__/network.js rename to packages/schemer/__tests__/network-test.ts index 1c69287a97..477c5ccdbe 100644 --- a/packages/schemer/__tests__/network.js +++ b/packages/schemer/__tests__/network-test.ts @@ -5,11 +5,11 @@ const S = new Schemer(schema, { rootDir: './__tests__' }); describe('Remote', () => { it('Icon', async () => { - await expect( - S.validateIcon( + expect( + await S.validateIcon( 'https://upload.wikimedia.org/wikipedia/commons/0/0f/Icon_Pinguin_2_512x512.png' ) - ).resolves; + ).toEqual(undefined); }); it('Remote icon dimensions correct', async () => { @@ -20,15 +20,20 @@ describe('Remote', () => { }, }, }); - await expect(S.validateIcon('https://httpbin.org/image/png')).resolves; + expect(await S.validateIcon('https://httpbin.org/image/png')).toEqual(undefined); }); it('Remote icon dimensions wrong', async () => { + let didError = false; const S = new Schemer( { properties: { icon: { - meta: { asset: true, dimensions: { width: 101, height: 100 } }, + meta: { + asset: true, + dimensions: { width: 101, height: 100 }, + contentTypePattern: '^image/png$', + }, }, }, }, @@ -37,8 +42,17 @@ describe('Remote', () => { try { await S.validateIcon('https://httpbin.org/image/png'); } catch (e) { + didError = true; expect(e).toBeTruthy(); expect(e.errors.length).toBe(1); + expect( + e.errors.map(validationError => { + const { stack, ...rest } = validationError; + return rest; + }) + ).toMatchSnapshot(); } + + expect(didError).toBe(true); }); }); diff --git a/packages/schemer/__tests__/test.js b/packages/schemer/__tests__/test.js deleted file mode 100644 index b283d95785..0000000000 --- a/packages/schemer/__tests__/test.js +++ /dev/null @@ -1,155 +0,0 @@ -/* eslint-disable import/order */ - -import { ErrorCodes, SchemerError } from '../src/Error'; -import Schemer from '../src/index'; - -describe('Sanity Tests', () => { - it('is a class', () => { - const schema = require('./files/schema.json'); - const S = new Schemer(schema); - expect(S instanceof Schemer).toBe(true); - }); - - it('has public functions', () => { - const schema = require('./files/schema.json'); - const S = new Schemer(schema); - expect(S.validateAll).toBeDefined(); - expect(S.validateProperty).toBeDefined(); - }); -}); - -const schema = require('./files/schema.json').schema; -const S = new Schemer(schema, { rootDir: './__tests__' }); -const good = require('./files/app.json'); -const bad = require('./files/bad.json'); -const badWithNot = require('./files/badwithnot.json'); - -describe('Holistic Unit Test', () => { - it('good example app.json all', async () => { - await expect(S.validateAll(good)).resolves; - }); - - it('good example app.json schema', async () => { - await expect(S.validateSchemaAsync(good)).resolves; - }); - - it('bad example app.json schema', async () => { - try { - await S.validateSchemaAsync(bad); - } catch (e) { - expect(e).toBeInstanceOf(SchemerError); - const errors = e.errors; - expect(errors.length).toBe(4); - expect( - errors.map(validationError => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { stack, ...rest } = validationError; - return rest; - }) - ).toMatchSnapshot(); - } - }); - - it('bad example app.json schema with field with not', async () => { - try { - await S.validateSchemaAsync(badWithNot); - } catch (e) { - expect(e).toBeInstanceOf(SchemerError); - const errors = e.errors; - expect(errors.length).toBe(1); - expect( - errors.map(validationError => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { stack, ...rest } = validationError; - return rest; - }) - ).toMatchSnapshot(); - } - }); -}); - -describe('Manual Validation Individual Unit Tests', () => { - it('Local Icon', async () => { - await expect(S.validateIcon('./files/check.png')).resolves; - }); - - it('Local Square Icon correct', async () => { - const S = new Schemer( - { properties: { icon: { meta: { asset: true, square: true } } } }, - { rootDir: './__tests__' } - ); - await expect(S.validateIcon('./files/check.png')).resolves; - }); - - it('Local icon dimensions wrong', async () => { - const S = new Schemer({ - properties: { - icon: { - meta: { asset: true, dimensions: { width: 400, height: 401 } }, - }, - }, - }); - try { - await S.validateIcon('./files/check.png'); - } catch (e) { - expect(e).toBeTruthy(); - expect(e.errors.length).toBe(1); - } - }); -}); - -describe('Individual Unit Tests', () => { - it('Error when missing Required Property', async () => { - const S = new Schemer({ - properties: { - name: {}, - }, - required: ['name'], - }); - try { - await S.validateAll({ noName: '' }); - } catch (e) { - expect(e.errors.length).toBe(1); - expect(e.errors[0].errorCode).toBe(ErrorCodes.SCHEMA_MISSING_REQUIRED_PROPERTY); - } - }); - - it('Error when data has an additional property', async () => { - const S = new Schemer({ additionalProperties: false }); - try { - await S.validateAll({ extraProperty: 'extra' }); - } catch (e) { - expect(e.errors.length).toBe(1); - expect(e.errors[0].errorCode).toBe(ErrorCodes.SCHEMA_ADDITIONAL_PROPERTY); - } - }); - - it('Name', async () => { - await expect(S.validateName('wilson')).resolves; - await expect(S.validateName([1, 2, 3, 4])).rejects.toBeDefined(); - await expect(S.validateName(23.232332)).rejects.toBeDefined(); - await expect(S.validateName(/regex.*/)).rejects.toBeDefined(); - }); - - xit('Slug', async () => { - await expect(S.validateSlug('wilson')).resolves; - await expect(S.validateSlug(12312123123)).rejects.toBeDefined(); - await expect(S.validateSlug([1, 23])).rejects.toBeDefined(); - - await expect(S.validateSlug('wilson123')).resolves; - await expect(S.validateSlug('wilson-123')).resolves; - await expect(S.validateSlug('wilson/test')).rejects.toBeDefined(); - await expect(S.validateSlug('wilson-test%')).rejects.toBeDefined(); - await expect(S.validateSlug('wilson-test-zhao--javascript-is-super-funky')).resolves; - }); - - xit('SDK Version', async () => { - await expect(S.validateSdkVersion('1.0.0')).resolves; - // TODO: is the following allowed? - await expect(S.validateSdkVersion('2.0.0.0.1')).rejects.toBeDefined(); - await expect(S.validateSdkVersion('UNVERSIONED')).resolves; - await expect(S.validateSdkVersion('12.2a.3')).rejects.toBeDefined(); - await expect(S.validateSdkVersion('9,9,9')).rejects.toBeDefined(); - await expect(S.validateSdkVersion('1.2')).rejects.toBeDefined(); - }); -}); diff --git a/packages/schemer/__tests__/test.ts b/packages/schemer/__tests__/test.ts new file mode 100644 index 0000000000..da0912b191 --- /dev/null +++ b/packages/schemer/__tests__/test.ts @@ -0,0 +1,291 @@ +import { ErrorCodes, SchemerError } from '../src/Error'; +import Schemer from '../src/index'; +import good from './files/app.json'; +import bad from './files/bad.json'; +import badWithNot from './files/badwithnot.json'; +import invalidAppIcon from './files/invalidAppIcon.json'; +import schema from './files/schema.json'; + +const S = new Schemer(schema.schema, { rootDir: './__tests__' }); + +describe('Sanity Tests', () => { + it('is a class', () => { + const schema = require('./files/schema.json'); + const S = new Schemer(schema, { rootDir: './__tests__' }); + expect(S instanceof Schemer).toBe(true); + }); + + it('has public functions', () => { + const schema = require('./files/schema.json'); + const S = new Schemer(schema, { rootDir: './__tests__' }); + expect(S.validateAll).toBeDefined(); + expect(S.validateProperty).toBeDefined(); + }); +}); + +describe('Image Validation', () => { + it('errors for webp images', async () => { + let didError = false; + try { + await S.validateAssetsAsync({ + android: { + adaptiveIcon: { foregroundImage: './files/webp.webp' }, + }, + }); + } catch (e) { + didError = true; + expect(e.errors[0].errorCode).toBe('INVALID_CONTENT_TYPE'); + expect(e.errors[1].errorCode).toBe('NOT_SQUARE'); + expect( + e.errors.map(validationError => { + const { stack, ...rest } = validationError; + return rest; + }) + ).toMatchSnapshot(); + } + + expect(didError).toBe(true); + }); + + it('errors when file extension and content do not match up', async () => { + let didError = false; + try { + await S.validateAssetsAsync({ + icon: './files/secretlyPng.jpg', + }); + } catch (e) { + didError = true; + expect(e.errors[0].errorCode).toBe('FILE_EXTENSION_MISMATCH'); + expect( + e.errors.map(validationError => { + const { stack, ...rest } = validationError; + return rest; + }) + ).toMatchSnapshot(); + } + + expect(didError).toBe(true); + }); +}); + +describe('Holistic Unit Test', () => { + it('good example app.json all', async () => { + expect(await S.validateAll(good)).toEqual(undefined); + }); + + it('good example app.json schema', async () => { + expect(await S.validateSchemaAsync(good)).toEqual(undefined); + }); + + it('bad example app.json schema', async () => { + let didError = false; + try { + await S.validateSchemaAsync(bad); + } catch (e) { + didError = true; + expect(e).toBeInstanceOf(SchemerError); + const errors = e.errors; + expect(errors.length).toBe(4); + expect( + errors.map(validationError => { + const { stack, ...rest } = validationError; + return rest; + }) + ).toMatchSnapshot(); + } + expect(didError).toBe(true); + }); + + it('bad example app.json schema with field with not', async () => { + let didError = false; + try { + await S.validateSchemaAsync(badWithNot); + } catch (e) { + didError = true; + expect(e).toBeInstanceOf(SchemerError); + const errors = e.errors; + expect(errors.length).toBe(1); + expect( + errors.map(validationError => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { stack, ...rest } = validationError; + return rest; + }) + ).toMatchSnapshot(); + } + expect(didError).toBe(true); + }); + + it('bad example app.json - invalid path for app icon', async () => { + let didError = false; + try { + await S.validateAll(invalidAppIcon); + } catch (e) { + didError = true; + expect(e).toBeInstanceOf(SchemerError); + const errors = e.errors; + expect(errors.length).toBe(1); + expect( + errors.map(validationError => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { stack, ...rest } = validationError; + return rest; + }) + ).toMatchSnapshot(); + } + expect(didError).toBe(true); + }); +}); + +describe('Manual Validation Individual Unit Tests', () => { + it('Local Icon', async () => { + expect(await S.validateIcon('./files/check.png')).toEqual(undefined); + }); + + it('Local Square Icon correct', async () => { + const S = new Schemer( + { properties: { icon: { meta: { asset: true, square: true } } } }, + { rootDir: './__tests__' } + ); + expect(await S.validateIcon('./files/check.png')).toEqual(undefined); + }); + + it('Local icon dimensions wrong', async () => { + let didError = false; + const S = new Schemer( + { + properties: { + icon: { + meta: { + asset: true, + dimensions: { width: 400, height: 401 }, + contentTypePattern: '^image/png$', + }, + }, + }, + }, + { rootDir: './__tests__' } + ); + try { + await S.validateIcon('./files/check.png'); + } catch (e) { + didError = true; + expect(e).toBeTruthy(); + expect(e.errors.length).toBe(1); + expect( + e.errors.map(validationError => { + const { stack, ...rest } = validationError; + return rest; + }) + ).toMatchSnapshot(); + } + expect(didError).toBe(true); + }); +}); + +describe('Individual Unit Tests', () => { + it('Error when missing Required Property', async () => { + let didError = false; + const S = new Schemer( + { + properties: { + name: {}, + }, + required: ['name'], + }, + { rootDir: './__tests__' } + ); + try { + await S.validateAll({ noName: '' }); + } catch (e) { + didError = true; + expect(e.errors.length).toBe(1); + expect(e.errors[0].errorCode).toBe(ErrorCodes.SCHEMA_MISSING_REQUIRED_PROPERTY); + expect( + e.errors.map(validationError => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { stack, ...rest } = validationError; + return rest; + }) + ).toMatchSnapshot(); + } + expect(didError).toBe(true); + }); + + it('Error when data has an additional property', async () => { + let didError = false; + const S = new Schemer({ additionalProperties: false }, { rootDir: './__tests__' }); + try { + await S.validateAll({ extraProperty: 'extra' }); + } catch (e) { + didError = true; + expect(e.errors.length).toBe(1); + expect(e.errors[0].errorCode).toBe(ErrorCodes.SCHEMA_ADDITIONAL_PROPERTY); + expect( + e.errors.map(validationError => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { stack, ...rest } = validationError; + return rest; + }) + ).toMatchSnapshot(); + } + expect(didError).toBe(true); + }); + + it.each` + name | expectedError + ${'wilson'} | ${undefined} + ${[1, 2, 3, 4]} | ${'must be string'} + ${23.232332} | ${'must be string'} + ${/regex.*/} | ${'must be string'} + `('validates name: $name', async ({ name, expectedError }) => { + let didError = false; + try { + expect(await S.validateName(name)).toBe(undefined); + } catch (e) { + didError = true; + expect(e.message).toBe(expectedError); + } + expect(didError).toBe(Boolean(expectedError)); + }); + + it.each` + slug | expectedError + ${'wilson'} | ${undefined} + ${12312123123} | ${'must be string'} + ${[1, 23]} | ${'must be string'} + ${'wilson123'} | ${undefined} + ${'wilson-123'} | ${undefined} + ${'wilson/test'} | ${'\'\' must match pattern "^[a-zA-Z0-9_\\-]+$"'} + ${'wilson-test%'} | ${'\'\' must match pattern "^[a-zA-Z0-9_\\-]+$"'} + ${'wilson-test-zhao--javascript-is-super-funky'} | ${undefined} + `('validates slug: $slug', async ({ slug, expectedError }) => { + let didError = false; + try { + expect(await S.validateSlug(slug)).toBe(undefined); + } catch (e) { + didError = true; + expect(e.message).toBe(expectedError); + } + expect(didError).toBe(Boolean(expectedError)); + }); + + it.each` + sdkVersion | expectedError + ${'1.0.0'} | ${undefined} + ${'2.0.0.0.1'} | ${undefined} + ${'UNVERSIONED'} | ${undefined} + ${'12.2a.3'} | ${'\'\' must match pattern "^(\\d+\\.\\d+\\.\\d+)|(UNVERSIONED)$"'} + ${'9,9,9'} | ${'\'\' must match pattern "^(\\d+\\.\\d+\\.\\d+)|(UNVERSIONED)$"'} + ${'1.2'} | ${'\'\' must match pattern "^(\\d+\\.\\d+\\.\\d+)|(UNVERSIONED)$"'} + `('validates SDK version: $sdkVersion', async ({ sdkVersion, expectedError }) => { + let didError = false; + try { + expect(await S.validateSdkVersion(sdkVersion)).toBe(undefined); + } catch (e) { + didError = true; + expect(e.message).toBe(expectedError); + } + expect(didError).toBe(Boolean(expectedError)); + }); +}); diff --git a/packages/schemer/package.json b/packages/schemer/package.json index 152c4d3539..c620a900a9 100644 --- a/packages/schemer/package.json +++ b/packages/schemer/package.json @@ -4,7 +4,7 @@ "description": "Centralized scheme validation library for Expo", "main": "./build/index.js", "scripts": { - "test": "jest --testPathIgnorePatterns network", + "test": "jest --testPathIgnorePatterns __tests__/network-test.ts", "test-integration": "jest", "watch": "tsc --watch --preserveWatchOutput", "build": "tsc", @@ -31,8 +31,7 @@ "ajv-formats": "^2.0.2", "json-schema-traverse": "^1.0.0", "lodash": "^4.17.21", - "probe-image-size": "^7.1.0", - "read-chunk": "^3.2.0" + "probe-image-size": "^7.1.0" }, "publishConfig": { "access": "public" diff --git a/packages/schemer/src/Error.ts b/packages/schemer/src/Error.ts index 3e4431d1e6..da488276d5 100644 --- a/packages/schemer/src/Error.ts +++ b/packages/schemer/src/Error.ts @@ -51,4 +51,5 @@ export const ErrorCodes = { INVALID_DIMENSIONS: 'INVALID_DIMENSIONS', INVALID_CONTENT_TYPE: 'INVALID_CONTENT_TYPE', NOT_SQUARE: 'NOT_SQUARE', + FILE_EXTENSION_MISMATCH: 'FILE_EXTENSION_MISMATCH', }; diff --git a/packages/schemer/src/index.ts b/packages/schemer/src/index.ts index 86ad35cb45..c66d9bde17 100644 --- a/packages/schemer/src/index.ts +++ b/packages/schemer/src/index.ts @@ -5,7 +5,6 @@ import traverse from 'json-schema-traverse'; import get from 'lodash/get'; import path from 'path'; import imageProbe from 'probe-image-size'; -import readChunk from 'read-chunk'; import { SchemerError, ValidationError } from './Error'; import { fieldPathToSchema, schemaPointerToFieldPath } from './Util'; @@ -183,21 +182,32 @@ export default class Schemer { // filePath could be an URL const filePath = path.resolve(this.rootDir, data); try { - // NOTE(nikki): The '4100' below should be enough for most file types, though we - // could probably go shorter.... - // http://www.garykessler.net/library/file_sigs.html - // The metadata content for .jpgs might be located a lot farther down the file, so this - // may pose problems in the future. // This cases on whether filePath is a remote URL or located on the machine - const probeResult = fs.existsSync(filePath) - ? imageProbe.sync(await readChunk(filePath, 0, 4100)) + const isLocalFile = fs.existsSync(filePath); + const probeResult = isLocalFile + ? await imageProbe(require('fs').createReadStream(filePath)) : await imageProbe(data, { useElectronNet: false }); + if (!probeResult) { return; } const { width, height, type, mime } = probeResult; + const fileExtension = filePath.split('.').pop(); + + if (isLocalFile && mime !== `image/${fileExtension}`) { + this.manualValidationErrors.push( + new ValidationError({ + errorCode: 'FILE_EXTENSION_MISMATCH', + fieldPath, + message: `the file extension should match the content, but the file extension is .${fileExtension} while the file content at '${data}' is of type ${type}`, + data, + meta, + }) + ); + } + if (contentTypePattern && !mime.match(new RegExp(contentTypePattern))) { this.manualValidationErrors.push( new ValidationError({ diff --git a/yarn.lock b/yarn.lock index a56b61c947..813a2eca76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14690,7 +14690,7 @@ p-try@^1.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= -p-try@^2.0.0, p-try@^2.1.0: +p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== @@ -15942,14 +15942,6 @@ react@^16.8.1: object-assign "^4.1.1" prop-types "^15.6.2" -read-chunk@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/read-chunk/-/read-chunk-3.2.0.tgz#2984afe78ca9bfbbdb74b19387bf9e86289c16ca" - integrity sha512-CEjy9LCzhmD7nUpJ1oVOE6s/hBkejlcJEgLQHVnQznOSilOPb+kpKktlLfFDK3/WP43+F80xkUTM2VOkYoSYvQ== - dependencies: - pify "^4.0.1" - with-open-file "^0.1.6" - read-cmd-shim@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz#87e43eba50098ba5a32d0ceb583ab8e43b961c16" @@ -19099,15 +19091,6 @@ windows-release@^3.1.0: dependencies: execa "^1.0.0" -with-open-file@^0.1.6: - version "0.1.7" - resolved "https://registry.yarnpkg.com/with-open-file/-/with-open-file-0.1.7.tgz#e2de8d974e8a8ae6e58886be4fe8e7465b58a729" - integrity sha512-ecJS2/oHtESJ1t3ZfMI3B7KIDKyfN0O16miWxdn30zdh66Yd3LsRFebXZXq6GU4xfxLf6nVxp9kIqElb5fqczA== - dependencies: - p-finally "^1.0.0" - p-try "^2.1.0" - pify "^4.0.1" - wonka@^4.0.14: version "4.0.15" resolved "https://registry.yarnpkg.com/wonka/-/wonka-4.0.15.tgz#9aa42046efa424565ab8f8f451fcca955bf80b89"