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 0000000000..9c34ad30b1 Binary files /dev/null and b/packages/schemer/__tests__/files/secretlyPng.jpg differ diff --git a/packages/schemer/__tests__/files/webp.webp b/packages/schemer/__tests__/files/webp.webp new file mode 100644 index 0000000000..0da983e2ce Binary files /dev/null and b/packages/schemer/__tests__/files/webp.webp differ 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"