@@ -259,7 +259,7 @@ import {
259259 writePendingInstallationSecrets ,
260260} from '../../src/cli/platform-registry.ts' ;
261261
262- const upgrade = ( overrides : { plan ?: boolean ; yes ?: boolean } = { } ) =>
262+ const upgrade = ( overrides : { plan ?: boolean ; yes ?: boolean ; customDomains ?: true } = { } ) =>
263263 runInstalledPlatformCommand ( {
264264 subcommand : 'upgrade' ,
265265 selector : state . manifest ! . id ,
@@ -921,6 +921,126 @@ describe('installed platform upgrade', () => {
921921 } ) ;
922922 } ) ;
923923
924+ describe ( 'enabling project custom domains' , ( ) => {
925+ const customHostnamePath = '/zones/zone/custom_hostnames?per_page=1' ;
926+
927+ beforeEach ( ( ) => {
928+ state . manifest = {
929+ ...state . manifest ! ,
930+ deployment : createPlatformDeploymentManifest ( {
931+ ...state . manifest ! . deployment ,
932+ application : { baseDomain : 'example.com' , zoneName : 'example.com' , dedicatedZone : true } ,
933+ } ) ,
934+ } ;
935+ const planner = vi . mocked ( createPlatformInstallPlan ) . getMockImplementation ( ) ! ;
936+ vi . mocked ( createPlatformInstallPlan ) . mockImplementationOnce ( async ( ...args ) => {
937+ const plan = await planner ( ...args ) ;
938+ return {
939+ ...plan ,
940+ deployment : createPlatformDeploymentManifest ( {
941+ ...plan . deployment ,
942+ capabilities : {
943+ ...plan . deployment . capabilities ,
944+ ...( args [ 1 ] . customDomains ? { customDomains : true } : { } ) ,
945+ } ,
946+ } ) ,
947+ zone : { id : 'zone' , name : 'example.com' , nameServers : [ ] , status : 'active' } ,
948+ } ;
949+ } ) ;
950+ state . createSecrets . mockReturnValue ( { cloudflareApiToken : 'saved-runtime-token' } ) ;
951+ state . request . mockImplementation ( async ( path : string ) => {
952+ state . events . push ( `request:${ path } ` ) ;
953+ return [ ] ;
954+ } ) ;
955+ } ) ;
956+
957+ it . each ( [ 'upgrade' , 'repair' ] as const ) (
958+ 'checks the retained token before %s can pause traffic' ,
959+ async ( subcommand ) => {
960+ await runInstalledPlatformCommand ( {
961+ subcommand,
962+ selector : state . manifest ! . id ,
963+ runtime : '' ,
964+ customDomains : true ,
965+ plan : false ,
966+ yes : true ,
967+ } ) ;
968+ expect ( state . clientTokens ) . toContain ( 'saved-runtime-token' ) ;
969+ const probe = state . events . indexOf ( `request:${ customHostnamePath } ` ) ;
970+ expect ( probe ) . toBeGreaterThan ( - 1 ) ;
971+ expect ( probe ) . toBeLessThan ( state . events . indexOf ( 'installer-start' ) ) ;
972+ expect ( state . createSecrets ) . toHaveBeenCalledWith (
973+ expect . objectContaining ( { cloudflareApiToken : '' } ) ,
974+ ) ;
975+ } ,
976+ ) ;
977+
978+ it . each ( [ 401 , 403 , 429 , 503 ] ) (
979+ 'stops before deployment when the token check returns %s' ,
980+ async ( status ) => {
981+ state . request . mockImplementation ( async ( path : string ) => {
982+ if ( path === customHostnamePath ) {
983+ throw new PlatformCloudflareApiError (
984+ 'upstream echoed saved-runtime-token' ,
985+ status ,
986+ path ,
987+ ) ;
988+ }
989+ return [ ] ;
990+ } ) ;
991+ const failure = await upgrade ( { customDomains : true } ) . catch ( ( error : Error ) => error ) ;
992+ expect ( failure ) . toBeInstanceOf ( Error ) ;
993+ expect ( String ( failure ) ) . toContain ( `HTTP ${ status } ` ) ;
994+ expect ( String ( failure ) ) . not . toContain ( 'saved-runtime-token' ) ;
995+ if ( status === 403 ) {
996+ expect ( String ( failure ) ) . toContain ( 'SSL and Certificates → Edit' ) ;
997+ }
998+ expect ( installPlatform ) . not . toHaveBeenCalled ( ) ;
999+ expect ( state . events . filter ( ( event ) => event . startsWith ( 'put:' ) ) ) . toEqual ( [ ] ) ;
1000+ } ,
1001+ ) ;
1002+
1003+ it ( 'requires a runtime token when enabling custom domains without saved credentials' , async ( ) => {
1004+ state . createSecrets . mockReturnValue ( { } ) ;
1005+ await expect ( upgrade ( { customDomains : true } ) ) . rejects . toThrow (
1006+ 'VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN' ,
1007+ ) ;
1008+ expect ( installPlatform ) . not . toHaveBeenCalled ( ) ;
1009+ } ) ;
1010+
1011+ it ( 'checks an explicit replacement instead of the retained token' , async ( ) => {
1012+ state . createSecrets . mockReturnValue ( {
1013+ cloudflareApiToken : 'replacement-token' ,
1014+ workerSecretUpdates : { CF_API_TOKEN : 'replacement-token' } ,
1015+ } ) ;
1016+ await upgrade ( { customDomains : true } ) ;
1017+ expect ( state . clientTokens ) . toContain ( 'replacement-token' ) ;
1018+ expect ( state . clientTokens ) . not . toContain ( 'saved-runtime-token' ) ;
1019+ expect ( state . events ) . toContain ( `request:${ customHostnamePath } ` ) ;
1020+ } ) ;
1021+
1022+ it ( 'does not check retained tokens when custom domains are already enabled' , async ( ) => {
1023+ state . manifest = {
1024+ ...state . manifest ! ,
1025+ deployment : createPlatformDeploymentManifest ( {
1026+ ...state . manifest ! . deployment ,
1027+ capabilities : { ...state . manifest ! . deployment . capabilities , customDomains : true } ,
1028+ } ) ,
1029+ } ;
1030+ await upgrade ( ) ;
1031+ expect ( state . clientTokens ) . not . toContain ( 'saved-runtime-token' ) ;
1032+ expect ( state . events ) . not . toContain ( `request:${ customHostnamePath } ` ) ;
1033+ } ) ;
1034+
1035+ it ( 'can plan enabling custom domains without a runtime token' , async ( ) => {
1036+ state . createSecrets . mockReturnValue ( { } ) ;
1037+ await upgrade ( { customDomains : true , plan : true } ) ;
1038+ expect ( state . clientTokens ) . not . toContain ( 'saved-runtime-token' ) ;
1039+ expect ( installPlatform ) . not . toHaveBeenCalled ( ) ;
1040+ expect ( state . events ) . not . toContain ( `request:${ customHostnamePath } ` ) ;
1041+ } ) ;
1042+ } ) ;
1043+
9241044 describe ( 'runtime token email access' , ( ) => {
9251045 const emailPaths = [
9261046 '/zones/mail-zone' ,
0 commit comments