@@ -294,6 +294,248 @@ process.exit(exitCode);
294294 } ) ;
295295 } ) ;
296296
297+ describe ( 'sentry.properties fallback' , ( ) => {
298+ let mockNpxScript : string ;
299+ let mockBinDir : string ;
300+
301+ beforeEach ( ( ) => {
302+ // Create a mock npx that makes `expo config --json` fail fast
303+ // so the script falls through to the sentry.properties fallback
304+ mockBinDir = path . join ( tempDir , 'mock-bin' ) ;
305+ fs . mkdirSync ( mockBinDir , { recursive : true } ) ;
306+ mockNpxScript = path . join ( mockBinDir , 'npx' ) ;
307+ fs . writeFileSync ( mockNpxScript , '#!/usr/bin/env node\nprocess.exit(1);\n' ) ;
308+ fs . chmodSync ( mockNpxScript , '755' ) ;
309+ } ) ;
310+
311+ const createSentryProperties = ( dir : string , content : string ) => {
312+ fs . mkdirSync ( dir , { recursive : true } ) ;
313+ fs . writeFileSync ( path . join ( dir , 'sentry.properties' ) , content ) ;
314+ } ;
315+
316+ const runScriptWithCwd = (
317+ cwd : string ,
318+ env : Record < string , string | undefined > = { } ,
319+ ) : { stdout : string ; stderr : string ; exitCode : number } => {
320+ const defaultEnv = {
321+ SENTRY_AUTH_TOKEN : 'test-token' ,
322+ SENTRY_CLI_EXECUTABLE : mockSentryCliScript ,
323+ // Put mock npx first in PATH so expo config fails fast
324+ PATH : `${ mockBinDir } :${ process . env . PATH } ` ,
325+ } ;
326+
327+ const result = spawnSync ( process . execPath , [ EXPO_UPLOAD_SCRIPT , outputDir ] , {
328+ cwd,
329+ env : { ...process . env , ...defaultEnv , ...env } ,
330+ encoding : 'utf8' ,
331+ timeout : 10000 ,
332+ } ) ;
333+
334+ return {
335+ stdout : result . stdout || '' ,
336+ stderr : result . stderr || '' ,
337+ exitCode : result . status || 0 ,
338+ } ;
339+ } ;
340+
341+ it ( 'reads config from android/sentry.properties when expo config is not available' , ( ) => {
342+ createAssets ( [ 'bundle.js' , 'bundle.js.map' ] ) ;
343+ createSentryProperties (
344+ path . join ( tempDir , 'android' ) ,
345+ 'defaults.url=https://sentry.io/\ndefaults.org=props-org\ndefaults.project=props-project\n' ,
346+ ) ;
347+
348+ const result = runScriptWithCwd ( tempDir , {
349+ SENTRY_ORG : undefined ,
350+ SENTRY_PROJECT : undefined ,
351+ SENTRY_URL : undefined ,
352+ MOCK_CLI_EXIT_CODE : '0' ,
353+ } ) ;
354+
355+ expect ( result . exitCode ) . toBe ( 0 ) ;
356+ expect ( result . stdout ) . toContain ( 'Found sentry properties in' ) ;
357+ expect ( result . stdout ) . toContain ( 'android' ) ;
358+ } ) ;
359+
360+ it ( 'reads config from ios/sentry.properties when android is not available' , ( ) => {
361+ createAssets ( [ 'bundle.js' , 'bundle.js.map' ] ) ;
362+ createSentryProperties (
363+ path . join ( tempDir , 'ios' ) ,
364+ 'defaults.url=https://sentry.io/\ndefaults.org=ios-org\ndefaults.project=ios-project\n' ,
365+ ) ;
366+
367+ const result = runScriptWithCwd ( tempDir , {
368+ SENTRY_ORG : undefined ,
369+ SENTRY_PROJECT : undefined ,
370+ SENTRY_URL : undefined ,
371+ MOCK_CLI_EXIT_CODE : '0' ,
372+ } ) ;
373+
374+ expect ( result . exitCode ) . toBe ( 0 ) ;
375+ expect ( result . stdout ) . toContain ( 'Found sentry properties in' ) ;
376+ expect ( result . stdout ) . toContain ( 'ios' ) ;
377+ } ) ;
378+
379+ it ( 'skips comment lines and empty lines in sentry.properties' , ( ) => {
380+ createAssets ( [ 'bundle.js' , 'bundle.js.map' ] ) ;
381+ createSentryProperties (
382+ path . join ( tempDir , 'android' ) ,
383+ '# This is a comment\ndefaults.url=https://sentry.io/\n\ndefaults.org=comment-org\n# Another comment\ndefaults.project=comment-project\n' ,
384+ ) ;
385+
386+ const result = runScriptWithCwd ( tempDir , {
387+ SENTRY_ORG : undefined ,
388+ SENTRY_PROJECT : undefined ,
389+ SENTRY_URL : undefined ,
390+ MOCK_CLI_EXIT_CODE : '0' ,
391+ } ) ;
392+
393+ expect ( result . exitCode ) . toBe ( 0 ) ;
394+ expect ( result . stdout ) . toContain ( 'Found sentry properties in' ) ;
395+ } ) ;
396+
397+ it ( 'fails with helpful message when no config source is available' , ( ) => {
398+ createAssets ( [ 'bundle.js' , 'bundle.js.map' ] ) ;
399+
400+ const result = runScriptWithCwd ( tempDir , {
401+ SENTRY_ORG : undefined ,
402+ SENTRY_PROJECT : undefined ,
403+ SENTRY_URL : undefined ,
404+ } ) ;
405+
406+ expect ( result . exitCode ) . toBe ( 1 ) ;
407+ const output = result . stdout + result . stderr ;
408+ expect ( output ) . toContain ( 'SENTRY_ORG' ) ;
409+ expect ( output ) . toContain ( 'SENTRY_PROJECT' ) ;
410+ } ) ;
411+ } ) ;
412+
413+ describe ( 'config._internal.sentryBuildProperties fallback (withSentry programmatic usage)' , ( ) => {
414+ const runScriptWithMockExpoConfig = (
415+ expoConfig : Record < string , unknown > ,
416+ env : Record < string , string | undefined > = { } ,
417+ ) : { stdout : string ; stderr : string ; exitCode : number } => {
418+ // Create a mock npx that outputs the given expo config as JSON
419+ const mockBinDir = path . join ( tempDir , 'mock-bin-expo' ) ;
420+ fs . mkdirSync ( mockBinDir , { recursive : true } ) ;
421+ const mockNpxScript = path . join ( mockBinDir , 'npx' ) ;
422+ // The mock npx script outputs the config JSON when called with 'expo config --json'
423+ fs . writeFileSync (
424+ mockNpxScript ,
425+ `#!/usr/bin/env node
426+ const args = process.argv.slice(2);
427+ if (args.includes('expo') && args.includes('config') && args.includes('--json')) {
428+ process.stdout.write(${ JSON . stringify ( JSON . stringify ( expoConfig ) ) } );
429+ process.exit(0);
430+ }
431+ process.exit(1);
432+ ` ,
433+ ) ;
434+ fs . chmodSync ( mockNpxScript , '755' ) ;
435+
436+ const defaultEnv = {
437+ SENTRY_AUTH_TOKEN : 'test-token' ,
438+ SENTRY_CLI_EXECUTABLE : mockSentryCliScript ,
439+ PATH : `${ mockBinDir } :${ process . env . PATH } ` ,
440+ } ;
441+
442+ const result = spawnSync ( process . execPath , [ EXPO_UPLOAD_SCRIPT , outputDir ] , {
443+ cwd : tempDir ,
444+ env : { ...process . env , ...defaultEnv , ...env } ,
445+ encoding : 'utf8' ,
446+ timeout : 10000 ,
447+ } ) ;
448+
449+ return {
450+ stdout : result . stdout || '' ,
451+ stderr : result . stderr || '' ,
452+ exitCode : result . status || 0 ,
453+ } ;
454+ } ;
455+
456+ it ( 'reads config from _internal.sentryBuildProperties when plugin is not in plugins array' , ( ) => {
457+ createAssets ( [ 'bundle.js' , 'bundle.js.map' ] ) ;
458+
459+ const result = runScriptWithMockExpoConfig (
460+ {
461+ plugins : [ [ 'some-other-plugin' , { } ] ] ,
462+ _internal : {
463+ sentryBuildProperties : {
464+ organization : 'internal-org' ,
465+ project : 'internal-project' ,
466+ url : 'https://sentry.io/' ,
467+ } ,
468+ } ,
469+ } ,
470+ {
471+ SENTRY_ORG : undefined ,
472+ SENTRY_PROJECT : undefined ,
473+ SENTRY_URL : undefined ,
474+ MOCK_CLI_EXIT_CODE : '0' ,
475+ } ,
476+ ) ;
477+
478+ expect ( result . exitCode ) . toBe ( 0 ) ;
479+ expect ( result . stdout ) . toContain ( 'Uploaded bundles and sourcemaps to Sentry successfully' ) ;
480+ } ) ;
481+
482+ it ( 'prefers plugins array over _internal.sentryBuildProperties' , ( ) => {
483+ createAssets ( [ 'bundle.js' , 'bundle.js.map' ] ) ;
484+
485+ const result = runScriptWithMockExpoConfig (
486+ {
487+ plugins : [
488+ [
489+ '@sentry/react-native/expo' ,
490+ { organization : 'plugin-org' , project : 'plugin-project' , url : 'https://sentry.io/' } ,
491+ ] ,
492+ ] ,
493+ _internal : {
494+ sentryBuildProperties : {
495+ organization : 'internal-org' ,
496+ project : 'internal-project' ,
497+ url : 'https://sentry.io/' ,
498+ } ,
499+ } ,
500+ } ,
501+ {
502+ SENTRY_ORG : undefined ,
503+ SENTRY_PROJECT : undefined ,
504+ SENTRY_URL : undefined ,
505+ MOCK_CLI_EXIT_CODE : '0' ,
506+ } ,
507+ ) ;
508+
509+ expect ( result . exitCode ) . toBe ( 0 ) ;
510+ expect ( result . stdout ) . toContain ( 'SENTRY_ORG resolved to plugin-org' ) ;
511+ } ) ;
512+
513+ it ( 'reads _internal.sentryBuildProperties even when plugins array is missing' , ( ) => {
514+ createAssets ( [ 'bundle.js' , 'bundle.js.map' ] ) ;
515+
516+ const result = runScriptWithMockExpoConfig (
517+ {
518+ _internal : {
519+ sentryBuildProperties : {
520+ organization : 'no-plugins-org' ,
521+ project : 'no-plugins-project' ,
522+ url : 'https://sentry.io/' ,
523+ } ,
524+ } ,
525+ } ,
526+ {
527+ SENTRY_ORG : undefined ,
528+ SENTRY_PROJECT : undefined ,
529+ SENTRY_URL : undefined ,
530+ MOCK_CLI_EXIT_CODE : '0' ,
531+ } ,
532+ ) ;
533+
534+ expect ( result . exitCode ) . toBe ( 0 ) ;
535+ expect ( result . stdout ) . toContain ( 'Uploaded bundles and sourcemaps to Sentry successfully' ) ;
536+ } ) ;
537+ } ) ;
538+
297539 describe ( 'sourcemap processing' , ( ) => {
298540 it ( 'converts debugId to debug_id in sourcemaps' , ( ) => {
299541 createAssets ( [ 'bundle.js' , 'bundle.js.map' ] ) ;
0 commit comments