@@ -389,3 +389,188 @@ describe("create, patch, and get email theme", () => {
389389 } ) ;
390390} ) ;
391391
392+ describe ( "invalid JSX inputs" , ( ) => {
393+ it ( "should reject theme that throws an error when rendered" , async ( { expect } ) => {
394+ await Project . createAndSwitch ( {
395+ display_name : "Test Email Theme Project" ,
396+ } ) ;
397+
398+ const response = await niceBackendFetch (
399+ `/api/latest/internal/email-themes/${ validThemeId } ` ,
400+ {
401+ method : "PATCH" ,
402+ accessType : "admin" ,
403+ body : {
404+ tsx_source : `
405+ import { Html } from '@react-email/components';
406+ export function EmailTheme({ children }: { children: React.ReactNode }) {
407+ throw new Error('Intentional error from theme');
408+ }
409+ ` ,
410+ } ,
411+ }
412+ ) ;
413+ expect ( response . status ) . toBe ( 400 ) ;
414+ expect ( response . body ) . toMatchInlineSnapshot ( "???" ) ;
415+ } ) ;
416+
417+ it ( "should reject theme that does not export EmailTheme function" , async ( { expect } ) => {
418+ await Project . createAndSwitch ( {
419+ display_name : "Test Email Theme Project" ,
420+ } ) ;
421+
422+ const response = await niceBackendFetch (
423+ `/api/latest/internal/email-themes/${ validThemeId } ` ,
424+ {
425+ method : "PATCH" ,
426+ accessType : "admin" ,
427+ body : {
428+ tsx_source : `
429+ import { Html } from '@react-email/components';
430+ export function WrongFunctionName({ children }: { children: React.ReactNode }) {
431+ return <Html>{children}</Html>;
432+ }
433+ ` ,
434+ } ,
435+ }
436+ ) ;
437+ expect ( response . status ) . toBe ( 400 ) ;
438+ expect ( response . body ) . toMatchInlineSnapshot ( `
439+ {
440+ "code": "EMAIL_RENDERING_ERROR",
441+ "details": {
442+ "error": deindent\`
443+ Build failed with 1 error:
444+ virtual:/render.tsx:9:9: ERROR: No matching export in "virtual:/theme.tsx" for import "EmailTheme"
445+ \`,
446+ },
447+ "error": deindent\`
448+ Failed to render email with theme: Build failed with 1 error:
449+ virtual:/render.tsx:9:9: ERROR: No matching export in "virtual:/theme.tsx" for import "EmailTheme"
450+ \`,
451+ }
452+ ` ) ;
453+ } ) ;
454+
455+ it ( "should reject theme with invalid JSX syntax" , async ( { expect } ) => {
456+ await Project . createAndSwitch ( {
457+ display_name : "Test Email Theme Project" ,
458+ } ) ;
459+
460+ const response = await niceBackendFetch (
461+ `/api/latest/internal/email-themes/${ validThemeId } ` ,
462+ {
463+ method : "PATCH" ,
464+ accessType : "admin" ,
465+ body : {
466+ tsx_source : `
467+ export function EmailTheme({ children }) {
468+ return <div><span>unclosed tag
469+ }
470+ ` ,
471+ } ,
472+ }
473+ ) ;
474+ expect ( response . status ) . toBe ( 400 ) ;
475+ expect ( response . body ) . toMatchInlineSnapshot ( `
476+ {
477+ "code": "EMAIL_RENDERING_ERROR",
478+ "details": {
479+ "error": deindent\`
480+ Build failed with 2 errors:
481+ virtual:/theme.tsx:4:12: ERROR: The character "}" is not valid inside a JSX element
482+ virtual:/theme.tsx:5:10: ERROR: Unexpected end of file before a closing "span" tag
483+ \`,
484+ },
485+ "error": deindent\`
486+ Failed to render email with theme: Build failed with 2 errors:
487+ virtual:/theme.tsx:4:12: ERROR: The character "}" is not valid inside a JSX element
488+ virtual:/theme.tsx:5:10: ERROR: Unexpected end of file before a closing "span" tag
489+ \`,
490+ }
491+ ` ) ;
492+ } ) ;
493+
494+ it . todo ( "should reject theme that causes infinite loop during rendering" , async ( { expect } ) => {
495+ await Project . createAndSwitch ( {
496+ display_name : "Test Email Theme Project" ,
497+ } ) ;
498+
499+ const response = await niceBackendFetch (
500+ `/api/latest/internal/email-themes/${ validThemeId } ` ,
501+ {
502+ method : "PATCH" ,
503+ accessType : "admin" ,
504+ body : {
505+ tsx_source : `
506+ import { Html } from '@react-email/components';
507+ export function EmailTheme({ children }: { children: React.ReactNode }) {
508+ while (true) {}
509+ return <Html>{children}</Html>;
510+ }
511+ ` ,
512+ } ,
513+ }
514+ ) ;
515+ // Should timeout or return an error, not hang indefinitely
516+ expect ( response . status ) . toBe ( 400 ) ;
517+ expect ( response . body ) . toMatchInlineSnapshot ( "todo" ) ;
518+ } ) ;
519+
520+ it . todo ( "should reject theme that allocates too much memory" , async ( { expect } ) => {
521+ await Project . createAndSwitch ( {
522+ display_name : "Test Email Theme Project" ,
523+ } ) ;
524+
525+ const response = await niceBackendFetch (
526+ `/api/latest/internal/email-themes/${ validThemeId } ` ,
527+ {
528+ method : "PATCH" ,
529+ accessType : "admin" ,
530+ body : {
531+ tsx_source : `
532+ import { Html } from '@react-email/components';
533+ export function EmailTheme({ children }: { children: React.ReactNode }) {
534+ const arr = [];
535+ for (let i = 0; i < 1e9; i++) {
536+ arr.push(new Array(1e6).fill('x'));
537+ }
538+ return <Html>{children}</Html>;
539+ }
540+ ` ,
541+ } ,
542+ }
543+ ) ;
544+ // Should fail due to memory limits, not hang or crash the server
545+ expect ( response . status ) . toBe ( 400 ) ;
546+ expect ( response . body ) . toMatchInlineSnapshot ( "todo" ) ;
547+ } ) ;
548+
549+ it ( "should reject theme that exports a non-function" , async ( { expect } ) => {
550+ await Project . createAndSwitch ( {
551+ display_name : "Test Email Theme Project" ,
552+ } ) ;
553+
554+ const response = await niceBackendFetch (
555+ `/api/latest/internal/email-themes/${ validThemeId } ` ,
556+ {
557+ method : "PATCH" ,
558+ accessType : "admin" ,
559+ body : {
560+ tsx_source : `
561+ export const EmailTheme = "not a function";
562+ ` ,
563+ } ,
564+ }
565+ ) ;
566+ expect ( response . status ) . toBe ( 400 ) ;
567+ expect ( response . body ) . toMatchInlineSnapshot ( `
568+ {
569+ "code": "EMAIL_RENDERING_ERROR",
570+ "details": { "error": "{\\"message\\":\\"element.type is not a function. (In 'element.type(element.props || {})', 'element.type' is \\\\\\"not a function\\\\\\")\\",\\"stack\\":\\"TypeError: element.type is not a function. (In 'element.type(element.props || {})', 'element.type' is \\\\\\"not a function\\\\\\")\\\\n at findComponentValue (/app/tmp/job-<stripped UUID>/script.ts:70:20)\\\\n at <anonymous> (/app/tmp/job-<stripped UUID>/script.ts:145:18)\\\\n at fulfilled (/app/tmp/job-<stripped UUID>/script.ts:32:24)\\"}" },
571+ "error": "Failed to render email with theme: {\\"message\\":\\"element.type is not a function. (In 'element.type(element.props || {})', 'element.type' is \\\\\\"not a function\\\\\\")\\",\\"stack\\":\\"TypeError: element.type is not a function. (In 'element.type(element.props || {})', 'element.type' is \\\\\\"not a function\\\\\\")\\\\n at findComponentValue (/app/tmp/job-<stripped UUID>/script.ts:70:20)\\\\n at <anonymous> (/app/tmp/job-<stripped UUID>/script.ts:145:18)\\\\n at fulfilled (/app/tmp/job-<stripped UUID>/script.ts:32:24)\\"}",
572+ }
573+ ` ) ;
574+ } ) ;
575+ } ) ;
576+
0 commit comments