1import { ExpoConfig } from '@expo/config-types';
2import glob from 'glob';
3import { vol } from 'memfs';
4import path from 'path';
5
6import { withEntitlementsPlist, withInfoPlist } from '../ios-plugins';
7import { evalModsAsync } from '../mod-compiler';
8import { getIosModFileProviders, withIosBaseMods } from '../withIosBaseMods';
9
10jest.mock('fs');
11jest.mock('glob');
12
13describe('entitlements', () => {
14  afterEach(() => {
15    vol.reset();
16  });
17
18  it(`evaluates in dry run mode`, async () => {
19    // Ensure this test runs in a blank file system
20    vol.fromJSON({});
21    let config: ExpoConfig = { name: 'bacon', slug: 'bacon' };
22    config = withEntitlementsPlist(config, (config) => {
23      config.modResults['haha'] = 'bet';
24      return config;
25    });
26
27    // base mods must be added last
28    config = withIosBaseMods(config, {
29      saveToInternal: true,
30      providers: {
31        entitlements: {
32          getFilePath() {
33            return '';
34          },
35          async read() {
36            return {};
37          },
38          async write() {},
39        },
40      },
41    });
42    config = await evalModsAsync(config, { projectRoot: '/', platforms: ['ios'] });
43
44    expect(config.ios?.entitlements).toStrictEqual({
45      haha: 'bet',
46    });
47    // @ts-ignore: mods are untyped
48    expect(config.mods.ios.entitlements).toBeDefined();
49
50    expect(config._internal.modResults.ios.entitlements).toBeDefined();
51
52    // Ensure no files were written
53    expect(vol.toJSON()).toStrictEqual({});
54  });
55
56  it('uses local entitlement files by default', async () => {
57    // Create a fake project that can load entitlements
58    vol.fromJSON({
59      '/ios/HelloWorld/AppDelegate.mm': 'Fake AppDelegate.mm',
60      '/ios/HelloWorld.xcodeproj/project.pbxproj': jest
61        .requireActual<typeof import('fs')>('fs')
62        .readFileSync(
63          path.resolve(__dirname, './fixtures/project-files/ios/project.pbxproj'),
64          'utf-8'
65        ),
66      '/ios/HelloWorld/HelloWorld.entitlements': jest
67        .requireActual<typeof import('fs')>('fs')
68        .readFileSync(
69          path.resolve(__dirname, './fixtures/project-files/ios/project.entitlements'),
70          'utf-8'
71        ),
72    });
73
74    // Mock glob response to "find" the memfs files
75    jest.mocked(glob.sync).mockImplementation((pattern) => {
76      if (pattern === 'ios/**/*.xcodeproj') return ['/ios/HelloWorld.xcodeproj'];
77      if (pattern === 'ios/*/AppDelegate.@(m|mm|swift)') return ['/ios/HelloWorld/AppDelegate.mm'];
78      throw new Error('Unexpected glob pattern used in test');
79    });
80
81    // Create simple project config and config plugin chain
82    let config: ExpoConfig = { name: 'bacon', slug: 'bacon' };
83    config = withEntitlementsPlist(config, (config) => {
84      config.modResults['haha'] = 'yes';
85      return config;
86    });
87
88    // Base mod must be added last
89    config = withIosBaseMods(config, {
90      saveToInternal: true,
91      providers: {
92        // Use the default mod provider, that's the one we need to test
93        entitlements: getIosModFileProviders().entitlements,
94      },
95    });
96    config = await evalModsAsync(config, {
97      projectRoot: '/',
98      platforms: ['ios'],
99    });
100
101    // Check if the generated entitlements are merged with local entitlements
102    expect(config.ios?.entitlements).toMatchInlineSnapshot(`
103      {
104        "aps-environment": "development",
105        "com.apple.developer.applesignin": [
106          "Default",
107        ],
108        "com.apple.developer.associated-domains": [
109          "applinks:acme.com",
110        ],
111        "com.apple.developer.icloud-container-identifiers": [
112          "iCloud.$(CFBundleIdentifier)",
113        ],
114        "com.apple.developer.icloud-services": [
115          "CloudDocuments",
116        ],
117        "com.apple.developer.ubiquity-container-identifiers": [
118          "iCloud.$(CFBundleIdentifier)",
119        ],
120        "com.apple.developer.ubiquity-kvstore-identifier": "$(TeamIdentifierPrefix)$(CFBundleIdentifier)",
121        "haha": "yes",
122      }
123    `);
124  });
125
126  it('skips local entitlements files when ignoring existing native files', async () => {
127    // Create a fake project that can load entitlements
128    vol.fromJSON({
129      '/ios/HelloWorld/AppDelegate.mm': 'Fake AppDelegate.mm',
130      '/ios/HelloWorld.xcodeproj/project.pbxproj': jest
131        .requireActual<typeof import('fs')>('fs')
132        .readFileSync(
133          path.resolve(__dirname, './fixtures/project-files/ios/project.pbxproj'),
134          'utf-8'
135        ),
136      '/ios/HelloWorld/HelloWorld.entitlements': jest
137        .requireActual<typeof import('fs')>('fs')
138        .readFileSync(
139          path.resolve(__dirname, './fixtures/project-files/ios/project.entitlements'),
140          'utf-8'
141        ),
142    });
143
144    // Mock glob response to "find" the memfs files
145    jest.mocked(glob.sync).mockImplementation((pattern) => {
146      if (pattern === 'ios/**/*.xcodeproj') return ['/ios/HelloWorld.xcodeproj'];
147      if (pattern === 'ios/*/AppDelegate.@(m|mm|swift)') return ['/ios/HelloWorld/AppDelegate.mm'];
148      throw new Error('Unexpected glob pattern used in test');
149    });
150
151    // Create simple project config and config plugin chain
152    let config: ExpoConfig = { name: 'bacon', slug: 'bacon' };
153    config = withEntitlementsPlist(config, (config) => {
154      config.modResults['haha'] = 'yes';
155      return config;
156    });
157
158    // Base mod must be added last
159    config = withIosBaseMods(config, {
160      saveToInternal: true,
161      providers: {
162        // Use the default mod provider, that's the one we need to test
163        entitlements: getIosModFileProviders().entitlements,
164      },
165    });
166    config = await evalModsAsync(config, {
167      projectRoot: '/',
168      platforms: ['ios'],
169      ignoreExistingNativeFiles: true,
170    });
171
172    // Check if the generated entitlements are NOT merged with local entitlements
173    expect(config.ios?.entitlements).toMatchInlineSnapshot(`
174      {
175        "haha": "yes",
176      }
177    `);
178  });
179});
180
181describe('infoPlist', () => {
182  afterEach(() => {
183    vol.reset();
184  });
185
186  it(`evaluates in dry run mode`, async () => {
187    // Ensure this test runs in a blank file system
188    vol.fromJSON({});
189    let config: ExpoConfig = { name: 'bacon', slug: 'bacon' };
190    config = withInfoPlist(config, (config) => {
191      config.modResults['haha'] = 'bet';
192      return config;
193    });
194
195    // base mods must be added last
196    config = withIosBaseMods(config, {
197      saveToInternal: true,
198      providers: {
199        infoPlist: {
200          getFilePath() {
201            return '';
202          },
203          async read() {
204            return {};
205          },
206          async write() {},
207        },
208      },
209    });
210    config = await evalModsAsync(config, { projectRoot: '/', platforms: ['ios'] });
211
212    expect(config.ios?.infoPlist).toStrictEqual({
213      haha: 'bet',
214    });
215    // @ts-ignore: mods are untyped
216    expect(config.mods.ios.infoPlist).toBeDefined();
217
218    expect(config._internal.modResults.ios.infoPlist).toBeDefined();
219
220    // Ensure no files were written
221    expect(vol.toJSON()).toStrictEqual({});
222  });
223});
224