-
Notifications
You must be signed in to change notification settings - Fork 98
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[eas-cli] Add metadata app store task runner (#1133)
* [eas-cli] Add metadata app store task runner * [eas-cli] Use expect error over ignore Co-authored-by: Evan Bacon <baconbrix@gmail.com> * [eas-cli] Remove unused ts directive * [eas-cli] Add download task tests Co-authored-by: Evan Bacon <baconbrix@gmail.com>
- Loading branch information
Showing
24 changed files
with
1,735 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import type { App } from '@expo/apple-utils'; | ||
|
||
import type { AgeRatingData } from './tasks/age-rating'; | ||
import type { AppInfoData } from './tasks/app-info'; | ||
import type { AppVersionData } from './tasks/app-version'; | ||
|
||
/** | ||
* The fully prepared apple data, used within the `downloadAsync` or `uploadAsync` tasks. | ||
* It contains references to each individual models, to either upload or download App Store data. | ||
*/ | ||
export type AppleData = { app: App } & AppInfoData & AppVersionData & AgeRatingData; | ||
|
||
/** | ||
* The unprepared partial apple data, used within the `prepareAsync` tasks. | ||
* It contains a reference to the app, each task should populate the necessary data. | ||
* If an entity fails to prepare the data, individual tasks should raise errors about the missing data. | ||
*/ | ||
export type PartialAppleData = { app: App } & Partial<Omit<AppleData, 'app'>>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { AppleConfigReader } from './config/reader'; | ||
import { AppleConfigWriter } from './config/writer'; | ||
import { AppleData, PartialAppleData } from './data'; | ||
|
||
export abstract class AppleTask { | ||
/** Get a description from the task to use as section headings in the log */ | ||
abstract name(): string; | ||
|
||
/** Prepare the data from the App Store to start syncing with the store configuration */ | ||
abstract prepareAsync(options: TaskPrepareOptions): Promise<void>; | ||
|
||
/** Download all information from the App Store to generate the store configuration */ | ||
abstract downloadAsync(options: TaskDownloadOptions): Promise<void>; | ||
|
||
/** Upload all information from the store configuration to the App Store */ | ||
abstract uploadAsync(options: TaskUploadOptions): Promise<void>; | ||
} | ||
|
||
export type TaskPrepareOptions = { | ||
context: PartialAppleData; | ||
}; | ||
|
||
export type TaskDownloadOptions = { | ||
config: AppleConfigWriter; | ||
context: AppleData; | ||
}; | ||
|
||
export type TaskUploadOptions = { | ||
config: AppleConfigReader; | ||
context: AppleData; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { getRequestClient } from '@expo/apple-utils'; | ||
|
||
export { default } from 'nock'; | ||
|
||
// Axios from apple-utils needs to be patched when we use Nock. | ||
// Instead of the default adapter, we need to use `axios/lib/adapters/http` | ||
// see: https://github.com/nock/nock#axios | ||
getRequestClient().defaults.adapter = require('axios/lib/adapters/http'); |
111 changes: 111 additions & 0 deletions
111
packages/eas-cli/src/metadata/apple/tasks/__tests__/age-rating.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import { AgeRatingDeclaration, AppStoreVersion, Rating } from '@expo/apple-utils'; | ||
import nock from 'nock'; | ||
|
||
import { AppleConfigReader } from '../../config/reader'; | ||
import { AppleConfigWriter } from '../../config/writer'; | ||
import { AppleData } from '../../data'; | ||
import { AgeRatingTask } from '../age-rating'; | ||
import { requestContext } from './fixtures/requestContext'; | ||
|
||
jest.mock('../../../../ora'); | ||
jest.mock('../../config/writer'); | ||
|
||
describe(AgeRatingTask, () => { | ||
describe('prepareAsync', () => { | ||
it('aborts when version is not loaded', async () => { | ||
const promise = new AgeRatingTask().prepareAsync({ context: {} as any }); | ||
|
||
await expect(promise).rejects.toThrow('not prepared'); | ||
}); | ||
|
||
it('loads age rating from app version', async () => { | ||
const scope = nock('https://api.appstoreconnect.apple.com') | ||
.get(`/v1/${AppStoreVersion.type}/stub-id/ageRatingDeclaration`) | ||
.reply(200, require('./fixtures/appStoreVersions/get-ageRatingDeclaration-200.json')); | ||
|
||
const context: any = { | ||
version: new AppStoreVersion(requestContext, 'stub-id', {} as any), | ||
}; | ||
|
||
await new AgeRatingTask().prepareAsync({ context }); | ||
|
||
expect(context.ageRating).toBeInstanceOf(AgeRatingDeclaration); | ||
expect(scope.isDone()).toBeTruthy(); | ||
}); | ||
}); | ||
|
||
describe('downloadAsync', () => { | ||
it('skips age rating when not prepared', async () => { | ||
const writer = jest.mocked(new AppleConfigWriter()); | ||
|
||
await new AgeRatingTask().downloadAsync({ | ||
config: writer, | ||
context: { ageRating: undefined } as any, | ||
}); | ||
|
||
expect(writer.setAgeRating).not.toBeCalled(); | ||
}); | ||
|
||
it('sets age rating when prepared', async () => { | ||
const writer = jest.mocked(new AppleConfigWriter()); | ||
const ageRating = new AgeRatingDeclaration(requestContext, 'stub-id', {} as any); | ||
|
||
await new AgeRatingTask().downloadAsync({ | ||
config: writer, | ||
context: { ageRating } as any, | ||
}); | ||
|
||
expect(writer.setAgeRating).toBeCalledWith(ageRating.attributes); | ||
}); | ||
}); | ||
|
||
describe('uploadAsync', () => { | ||
it('aborts when age rating is not loaded', async () => { | ||
const promise = new AgeRatingTask().uploadAsync({ | ||
config: new AppleConfigReader({}), | ||
context: { ageRating: undefined } as any, | ||
}); | ||
|
||
await expect(promise).rejects.toThrow('rating not initialized'); | ||
}); | ||
|
||
it('skips updating age rating when not configured', async () => { | ||
const scope = nock('https://api.appstoreconnect.apple.com') | ||
.patch(`/v1/${AgeRatingDeclaration.type}/stub-id`) | ||
.reply(200, require('./fixtures/ageRatingDeclarations/patch-200.json')); | ||
|
||
await new AgeRatingTask().uploadAsync({ | ||
config: new AppleConfigReader({ advisory: undefined }), | ||
context: { | ||
ageRating: new AgeRatingDeclaration(requestContext, 'stub-id', {} as any), | ||
} as AppleData, | ||
}); | ||
|
||
expect(scope.isDone()).toBeFalsy(); | ||
nock.cleanAll(); | ||
}); | ||
|
||
it('updates age rating from config when configured', async () => { | ||
const scope = nock('https://api.appstoreconnect.apple.com') | ||
.patch(`/v1/${AgeRatingDeclaration.type}/stub-id`) | ||
.reply(200, require('./fixtures/ageRatingDeclarations/patch-200.json')); | ||
|
||
const context = { | ||
ageRating: new AgeRatingDeclaration(requestContext, 'stub-id', {} as any), | ||
}; | ||
|
||
await new AgeRatingTask().uploadAsync({ | ||
config: new AppleConfigReader({ | ||
advisory: { | ||
// See fixture value for horrorOrFearThemes | ||
horrorOrFearThemes: Rating.INFREQUENT_OR_MILD, | ||
}, | ||
}), | ||
context: context as AppleData, | ||
}); | ||
|
||
expect(context.ageRating.id).not.toMatch('stub-id'); | ||
expect(scope.isDone()).toBeTruthy(); | ||
}); | ||
}); | ||
}); |
173 changes: 173 additions & 0 deletions
173
packages/eas-cli/src/metadata/apple/tasks/__tests__/app-info.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
import { App, AppCategoryId, AppInfo, AppInfoLocalization } from '@expo/apple-utils'; | ||
import nock from 'nock'; | ||
|
||
import { AppleConfigReader } from '../../config/reader'; | ||
import { AppleConfigWriter } from '../../config/writer'; | ||
import { AppleData, PartialAppleData } from '../../data'; | ||
import { AppInfoTask } from '../app-info'; | ||
import { requestContext } from './fixtures/requestContext'; | ||
|
||
jest.mock('../../../../ora'); | ||
jest.mock('../../config/writer'); | ||
|
||
describe(AppInfoTask, () => { | ||
describe('prepareAsync', () => { | ||
it('loads editable app info and locales from app instance', async () => { | ||
const scopeInfo = nock('https://api.appstoreconnect.apple.com') | ||
.get(uri => uri.startsWith(`/v1/${App.type}/stub-id/${AppInfo.type}`)) // allow any query params | ||
.reply(200, require('./fixtures/apps/get-appInfos-200.json')); | ||
|
||
const scopeLocales = nock('https://api.appstoreconnect.apple.com') | ||
.get(/\/v1\/appInfos\/.*\/appInfoLocalizations/) // allow any id from fixture | ||
.reply(200, require('./fixtures/appInfos/get-appInfoLocalizations-200.json')); | ||
|
||
const context: PartialAppleData = { | ||
app: new App(requestContext, 'stub-id', {} as any), | ||
}; | ||
|
||
await new AppInfoTask().prepareAsync({ context }); | ||
|
||
expect(context.info).toBeInstanceOf(AppInfo); | ||
expect(context.infoLocales).toBeInstanceOf(Array); | ||
expect(scopeInfo.isDone()).toBeTruthy(); | ||
expect(scopeLocales.isDone()).toBeTruthy(); | ||
}); | ||
}); | ||
|
||
describe('downloadAsync', () => { | ||
it('aborts when app info is not loaded', async () => { | ||
const writer = jest.mocked(new AppleConfigWriter()); | ||
const promise = new AppInfoTask().downloadAsync({ | ||
config: writer, | ||
context: { info: undefined, infoLocales: [] } as any, | ||
}); | ||
|
||
await expect(promise).rejects.toThrow('info not initialized'); | ||
}); | ||
|
||
it('sets categories when info is loaded', async () => { | ||
const writer = jest.mocked(new AppleConfigWriter()); | ||
const info = new AppInfo(requestContext, 'stub-id', {} as any); | ||
|
||
await new AppInfoTask().downloadAsync({ | ||
config: writer, | ||
context: { info, infoLocales: [] } as any, | ||
}); | ||
|
||
expect(writer.setCategories).toBeCalledWith(info.attributes); | ||
}); | ||
|
||
it('skips when no locales are loaded', async () => { | ||
const writer = jest.mocked(new AppleConfigWriter()); | ||
|
||
await new AppInfoTask().downloadAsync({ | ||
config: writer, | ||
context: { | ||
info: new AppInfo(requestContext, 'stub-id', {} as any), | ||
infoLocales: [], | ||
} as any, | ||
}); | ||
|
||
expect(writer.setInfoLocale).not.toBeCalled(); | ||
}); | ||
|
||
it('sets locales when loaded', async () => { | ||
const writer = jest.mocked(new AppleConfigWriter()); | ||
const infoLocales = [ | ||
new AppInfoLocalization(requestContext, 'stub-id-1', {} as any), | ||
new AppInfoLocalization(requestContext, 'stub-id-2', {} as any), | ||
]; | ||
|
||
await new AppInfoTask().downloadAsync({ | ||
config: writer, | ||
context: { | ||
info: new AppInfo(requestContext, 'stub-id', {} as any), | ||
infoLocales, | ||
} as any, | ||
}); | ||
|
||
expect(writer.setInfoLocale).toBeCalledWith(infoLocales[0].attributes); | ||
expect(writer.setInfoLocale).toBeCalledWith(infoLocales[1].attributes); | ||
}); | ||
}); | ||
|
||
describe('uploadAsync', () => { | ||
it('aborts when app info is not loaded', async () => { | ||
const promise = new AppInfoTask().uploadAsync({ | ||
config: new AppleConfigReader({}), | ||
context: { info: undefined } as any, | ||
}); | ||
|
||
await expect(promise).rejects.toThrow('info not initialized'); | ||
}); | ||
|
||
it('skips updating categories when not configured', async () => { | ||
const scope = nock('https://api.appstoreconnect.apple.com') | ||
.patch(`/v1/${AppInfo.type}/stub-id`) | ||
.reply(200, require('./fixtures/appInfos/patch-200.json')); | ||
|
||
await new AppInfoTask().uploadAsync({ | ||
config: new AppleConfigReader({ categories: undefined }), | ||
context: { | ||
info: new AppInfo(requestContext, 'stub-id', {} as any), | ||
} as AppleData, | ||
}); | ||
|
||
expect(scope.isDone()).toBeFalsy(); | ||
nock.cleanAll(); | ||
}); | ||
|
||
it('skips updating localized info when not configured', async () => { | ||
const scope = nock('https://api.appstoreconnect.apple.com') | ||
.patch(`/v1/${AppInfoLocalization.type}/stub-id`) | ||
.reply(200, require('./fixtures/appInfos/patch-200.json')); | ||
|
||
await new AppInfoTask().uploadAsync({ | ||
config: new AppleConfigReader({ info: undefined }), | ||
context: { | ||
info: new AppInfo(requestContext, 'stub-id', {} as any), | ||
} as AppleData, | ||
}); | ||
|
||
expect(scope.isDone()).toBeFalsy(); | ||
nock.cleanAll(); | ||
}); | ||
|
||
it('updates categories and localized info', async () => { | ||
const updateCategoryScope = nock('https://api.appstoreconnect.apple.com') | ||
.patch(`/v1/${AppInfo.type}/stub-id`) | ||
.reply(200, require('./fixtures/appInfos/patch-200.json')); | ||
|
||
const updateENInfoScope = nock('https://api.appstoreconnect.apple.com') | ||
.patch(`/v1/${AppInfoLocalization.type}/APP_INFO_LOCALE_1`) // see fixture ID | ||
.reply(200, require('./fixtures/appInfoLocalizations/patch-200.json')); | ||
|
||
const updateNLInfoScope = nock('https://api.appstoreconnect.apple.com') | ||
.patch(`/v1/${AppInfoLocalization.type}/APP_INFO_LOCALE_2`) // see fixture ID | ||
.reply(200, require('./fixtures/appInfoLocalizations/patch-200.json')); | ||
|
||
const fetchLocalizedInfoScope = nock('https://api.appstoreconnect.apple.com') | ||
.get(`/v1/${AppInfo.type}/APP_INFO_1/${AppInfoLocalization.type}`) // see fixture ID | ||
.twice() | ||
.reply(200, require('./fixtures/appInfos/get-appInfoLocalizations-200.json')); | ||
|
||
await new AppInfoTask().uploadAsync({ | ||
config: new AppleConfigReader({ | ||
categories: [AppCategoryId.ENTERTAINMENT], | ||
info: { | ||
'en-US': { title: 'Hello' }, | ||
'nl-NL': { title: 'Hallo' }, | ||
}, | ||
}), | ||
context: { | ||
info: new AppInfo(requestContext, 'stub-id', {} as any), | ||
} as AppleData, | ||
}); | ||
|
||
expect(updateCategoryScope.isDone()).toBeTruthy(); | ||
expect(fetchLocalizedInfoScope.isDone()).toBeTruthy(); | ||
expect(updateENInfoScope.isDone()).toBeTruthy(); | ||
expect(updateNLInfoScope.isDone()).toBeTruthy(); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.