@@ -5,7 +5,7 @@ import { http, HttpResponse } from 'msw';
55import { afterAll , afterEach , beforeAll , describe , expect , MockInstance , test , vi } from 'vitest' ;
66
77import { server } from '../../../tests/mocks/node' ;
8- import { createDomain , deleteDomain , getDomain , listDomains } from '../lib/domains' ;
8+ import { claimDomain , createDomain , deleteDomain , getDomain , listDomains } from '../lib/domains' ;
99import { consola } from '../lib/logger' ;
1010import { mkTempDir } from '../lib/mk-temp-dir' ;
1111import { getProjectConfig , ProjectConfigSchema } from '../lib/project-config' ;
@@ -66,7 +66,7 @@ afterAll(async () => {
6666} ) ;
6767
6868describe ( 'command configuration' , ( ) => {
69- test ( 'domains has add, list, status, and remove subcommands' , ( ) => {
69+ test ( 'domains has add, list, status, claim, and remove subcommands' , ( ) => {
7070 expect ( domains ) . toBeInstanceOf ( Command ) ;
7171 expect ( domains . name ( ) ) . toBe ( 'domains' ) ;
7272 expect ( domains . description ( ) ) . toBe (
@@ -76,6 +76,7 @@ describe('command configuration', () => {
7676 const add = domains . commands . find ( ( command ) => command . name ( ) === 'add' ) ;
7777 const list = domains . commands . find ( ( command ) => command . name ( ) === 'list' ) ;
7878 const status = domains . commands . find ( ( command ) => command . name ( ) === 'status' ) ;
79+ const claim = domains . commands . find ( ( command ) => command . name ( ) === 'claim' ) ;
7980 const remove = domains . commands . find ( ( command ) => command . name ( ) === 'remove' ) ;
8081
8182 expect ( add ) . toBeDefined ( ) ;
@@ -117,6 +118,20 @@ describe('command configuration', () => {
117118 ] ) ,
118119 ) ;
119120
121+ expect ( claim ) . toBeDefined ( ) ;
122+ expect ( claim ?. description ( ) ) . toBe (
123+ 'Claim a custom domain that is currently in use on another store.' ,
124+ ) ;
125+ expect ( claim ?. options ) . toEqual (
126+ expect . arrayContaining ( [
127+ expect . objectContaining ( { flags : '--store-hash <hash>' } ) ,
128+ expect . objectContaining ( { flags : '--access-token <token>' } ) ,
129+ expect . objectContaining ( { flags : '--api-host <host>' } ) ,
130+ expect . objectContaining ( { flags : '--project-uuid <uuid>' } ) ,
131+ expect . objectContaining ( { flags : '--wait' } ) ,
132+ ] ) ,
133+ ) ;
134+
120135 expect ( remove ) . toBeDefined ( ) ;
121136 expect ( remove ?. description ( ) ) . toBe (
122137 'Remove a custom domain from the current Native Hosting project.' ,
@@ -336,6 +351,62 @@ describe('domain API client', () => {
336351 ) . resolves . toBeUndefined ( ) ;
337352 expect ( deletedDomain ) . toBe ( domain ) ;
338353 } ) ;
354+
355+ test ( 'claims a domain' , async ( ) => {
356+ let claimedDomain : string | readonly string [ ] | undefined ;
357+
358+ server . use (
359+ http . post (
360+ 'https://:apiHost/stores/:storeHash/v3/infrastructure/projects/:projectUuid/domains/:domain/claim' ,
361+ ( { params } ) => {
362+ claimedDomain = params . domain ;
363+
364+ return new HttpResponse ( null , { status : 204 } ) ;
365+ } ,
366+ ) ,
367+ ) ;
368+
369+ await expect (
370+ claimDomain ( domain , projectUuid , storeHash , accessToken , apiHost ) ,
371+ ) . resolves . toBeUndefined ( ) ;
372+ expect ( claimedDomain ) . toBe ( domain ) ;
373+ } ) ;
374+
375+ test ( 'surfaces the ownership-verification TXT record when a claim is not yet verified' , async ( ) => {
376+ server . use (
377+ http . post (
378+ 'https://:apiHost/stores/:storeHash/v3/infrastructure/projects/:projectUuid/domains/:domain/claim' ,
379+ ( ) =>
380+ HttpResponse . json (
381+ {
382+ title : 'Domain ownership could not be verified.' ,
383+ errors : {
384+ domain : `No ownership verification TXT record was found at '_bigcommerce-verification.${ domain } '. Add the record, then try again.` ,
385+ } ,
386+ meta : {
387+ ownership_verification : {
388+ type : 'TXT' ,
389+ name : `_bigcommerce-verification.${ domain } ` ,
390+ value : 'bc-verify=019500e2933d70578e81090dd7240795' ,
391+ } ,
392+ } ,
393+ } ,
394+ { status : 422 } ,
395+ ) ,
396+ ) ,
397+ ) ;
398+
399+ await expect (
400+ claimDomain ( domain , projectUuid , storeHash , accessToken , apiHost ) ,
401+ ) . rejects . toMatchObject ( {
402+ name : 'DomainOwnershipVerificationError' ,
403+ ownershipVerification : {
404+ type : 'TXT' ,
405+ name : `_bigcommerce-verification.${ domain } ` ,
406+ value : 'bc-verify=019500e2933d70578e81090dd7240795' ,
407+ } ,
408+ } ) ;
409+ } ) ;
339410} ) ;
340411
341412describe ( 'waitForDomainVerification' , ( ) => {
@@ -446,6 +517,51 @@ describe('add command', () => {
446517 expect ( consola . start ) . toHaveBeenCalledWith ( `Waiting for ${ domain } to verify...` ) ;
447518 expect ( consola . log ) . toHaveBeenCalledWith ( expect . stringContaining ( 'active' ) ) ;
448519 } ) ;
520+
521+ test ( 'surfaces the ownership TXT record and claim guidance on a cross-store collision' , async ( ) => {
522+ server . use (
523+ http . post (
524+ 'https://:apiHost/stores/:storeHash/v3/infrastructure/projects/:projectUuid/domains' ,
525+ ( ) =>
526+ HttpResponse . json (
527+ {
528+ title :
529+ 'The domain is already bound to a different store. Verify ownership using the claim endpoint, then try again.' ,
530+ errors : {
531+ domain : `'${ domain } ' is already bound to a different store; verify ownership, then try again.` ,
532+ } ,
533+ meta : {
534+ ownership_verification : {
535+ type : 'TXT' ,
536+ name : `_bigcommerce-verification.${ domain } ` ,
537+ value : 'bc-verify=019500e2933d70578e81090dd7240795' ,
538+ } ,
539+ } ,
540+ } ,
541+ { status : 409 } ,
542+ ) ,
543+ ) ,
544+ ) ;
545+
546+ writeCredentials ( ) ;
547+
548+ await domains . parseAsync ( [ 'add' , domain ] , { from : 'user' } ) ;
549+
550+ expect ( consola . warn ) . toHaveBeenCalledWith (
551+ expect . stringContaining ( 'already in use on another store' ) ,
552+ ) ;
553+ expect ( consola . log ) . toHaveBeenCalledWith (
554+ expect . stringContaining ( 'bc-verify=019500e2933d70578e81090dd7240795' ) ,
555+ ) ;
556+ expect ( consola . info ) . toHaveBeenCalledWith (
557+ `Once the record is live, run: catalyst domains claim ${ domain } ` ,
558+ ) ;
559+ // The raw V3 title/field text isn't echoed — the concise message replaces it.
560+ expect ( consola . warn ) . not . toHaveBeenCalledWith (
561+ expect . stringContaining ( 'Verify ownership using the claim endpoint' ) ,
562+ ) ;
563+ expect ( exitMock ) . toHaveBeenCalledWith ( 1 ) ;
564+ } ) ;
449565} ) ;
450566
451567describe ( 'list command' , ( ) => {
@@ -553,6 +669,90 @@ describe('status command', () => {
553669 } ) ;
554670} ) ;
555671
672+ describe ( 'claim command' , ( ) => {
673+ test ( 'claims a domain for the linked project' , async ( ) => {
674+ writeCredentials ( ) ;
675+
676+ await domains . parseAsync ( [ 'claim' , domain ] , { from : 'user' } ) ;
677+
678+ expect ( consola . start ) . toHaveBeenCalledWith ( `Claiming domain ${ domain } ...` ) ;
679+ expect ( consola . success ) . toHaveBeenCalledWith ( `Domain ${ domain } claimed.` ) ;
680+ expect ( consola . log ) . toHaveBeenCalledWith ( expect . stringContaining ( domain ) ) ;
681+ expect ( consola . log ) . toHaveBeenCalledWith ( expect . stringContaining ( 'active' ) ) ;
682+ expect ( exitMock ) . toHaveBeenCalledWith ( 0 ) ;
683+ } ) ;
684+
685+ test ( 'prints the ownership TXT record when verification has not completed' , async ( ) => {
686+ server . use (
687+ http . post (
688+ 'https://:apiHost/stores/:storeHash/v3/infrastructure/projects/:projectUuid/domains/:domain/claim' ,
689+ ( ) =>
690+ HttpResponse . json (
691+ {
692+ title : 'Domain ownership could not be verified.' ,
693+ errors : {
694+ domain : `No ownership verification TXT record was found at '_bigcommerce-verification.${ domain } '. Add the record, then try again.` ,
695+ } ,
696+ meta : {
697+ ownership_verification : {
698+ type : 'TXT' ,
699+ name : `_bigcommerce-verification.${ domain } ` ,
700+ value : 'bc-verify=019500e2933d70578e81090dd7240795' ,
701+ } ,
702+ } ,
703+ } ,
704+ { status : 422 } ,
705+ ) ,
706+ ) ,
707+ ) ;
708+
709+ writeCredentials ( ) ;
710+
711+ await domains . parseAsync ( [ 'claim' , domain ] , { from : 'user' } ) ;
712+
713+ expect ( consola . warn ) . toHaveBeenCalledWith ( expect . stringContaining ( 'could not be verified' ) ) ;
714+ expect ( consola . log ) . toHaveBeenCalledWith (
715+ expect . stringContaining ( 'bc-verify=019500e2933d70578e81090dd7240795' ) ,
716+ ) ;
717+ expect ( consola . success ) . not . toHaveBeenCalled ( ) ;
718+ expect ( exitMock ) . toHaveBeenCalledWith ( 1 ) ;
719+ } ) ;
720+
721+ test ( 'can wait for a claimed domain to leave pending status' , async ( ) => {
722+ let getRequests = 0 ;
723+
724+ server . use (
725+ http . post (
726+ 'https://:apiHost/stores/:storeHash/v3/infrastructure/projects/:projectUuid/domains/:domain/claim' ,
727+ ( ) => new HttpResponse ( null , { status : 204 } ) ,
728+ ) ,
729+ http . get (
730+ 'https://:apiHost/stores/:storeHash/v3/infrastructure/projects/:projectUuid/domains/:domain' ,
731+ ( ) => {
732+ getRequests += 1 ;
733+
734+ return HttpResponse . json ( {
735+ data : {
736+ domain,
737+ project_uuid : projectUuid ,
738+ verification_status : getRequests === 1 ? 'pending' : 'verified' ,
739+ } ,
740+ } ) ;
741+ } ,
742+ ) ,
743+ ) ;
744+
745+ writeCredentials ( ) ;
746+
747+ await domains . parseAsync ( [ 'claim' , domain , '--wait' ] , { from : 'user' } ) ;
748+
749+ expect ( consola . success ) . toHaveBeenCalledWith ( `Domain ${ domain } claimed.` ) ;
750+ expect ( consola . start ) . toHaveBeenCalledWith ( `Waiting for ${ domain } to verify...` ) ;
751+ expect ( consola . log ) . toHaveBeenCalledWith ( expect . stringContaining ( 'active' ) ) ;
752+ expect ( getRequests ) . toBe ( 2 ) ;
753+ } ) ;
754+ } ) ;
755+
556756describe ( 'remove command' , ( ) => {
557757 test ( 'confirms before removing an active domain' , async ( ) => {
558758 confirmMock . mockResolvedValue ( true ) ;
0 commit comments