Coverage for src/fastapi_cloud_cli/commands/domains/_setup.py: 100%

97 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-11-09 00:25 +0000

1from dataclasses import dataclass 1CDEFGHI

2from typing import Literal 1CDEFGHI

3 

4from fastapi_cloud_cli.api import ( 1CDEFGHI

5 CustomDomain, 

6 CustomDomainRecord, 

7 CustomDomainStatus, 

8) 

9 

10StepStatus = Literal["verified", "in_progress", "attention", "locked", "failed"] 1CDEFGHI

11StepID = Literal["ownership", "certificate", "traffic", "combined"] 1CDEFGHI

12PhasedStepID = Literal["ownership", "certificate", "traffic"] 1CDEFGHI

13RecordGroup = PhasedStepID 1CDEFGHI

14Phase = Literal["internal", "external", "origin"] 1CDEFGHI

15 

16 

17@dataclass(frozen=True) 1CDEFGHI

18class SetupStep: 1CDEFGHI

19 id: StepID 1CDEFGHI

20 title: str 1CDEFGHI

21 description: str 1CDEFGHI

22 status: StepStatus 1CDEFGHI

23 records: list[CustomDomainRecord] 1CDEFGHI

24 

25 

26ATTENTION_STATUSES = frozenset( 1CDEFGHI

27 { 

28 CustomDomainStatus.internal_dcv_invalid, 

29 CustomDomainStatus.origin_setup_invalid, 

30 } 

31) 

32 

33FAILED_STATUSES = frozenset( 1CDEFGHI

34 { 

35 CustomDomainStatus.internal_dcv_timeout, 

36 CustomDomainStatus.internal_dcv_revoked, 

37 CustomDomainStatus.external_dcv_blocked, 

38 CustomDomainStatus.external_dcv_timeout, 

39 CustomDomainStatus.origin_setup_timeout, 

40 CustomDomainStatus.origin_setup_removed, 

41 } 

42) 

43 

44PHASE_ORDER: dict[Phase, int] = { 1CDEFGHI

45 "internal": 0, 

46 "external": 1, 

47 "origin": 2, 

48} 

49 

50 

51def _phase_from_status(status: CustomDomainStatus) -> Phase: 1CDEFGHI

52 if status.value.startswith("internal_dcv"): 1ohpaqirbsjtcukvdwlxeymzfAnBg

53 return "internal" 1ohpaqirbsjtcukvdwlxeymzfAnBg

54 if status.value.startswith("external_dcv"): 1haibjckdlemfng

55 return "external" 1haibjckdlemfng

56 return "origin" 1haibjckdlemfng

57 

58 

59def _record_group(record: CustomDomainRecord) -> RecordGroup: 1CDEFGHI

60 if record.type == "A": 1ohpaqirbsjtcukvdwlxeymzfAnBg

61 return "traffic" 1abcdefg

62 if record.type == "TXT": 1ohpaqirbsjtcukvdwlxeymzfAnBg

63 return "ownership" if "_fc-dcv" in (record.name or "") else "certificate" 1ohpaqirbsjtcukvdwlxeymzfAnBg

64 return "certificate" if "_acme-challenge" in (record.name or "") else "traffic" 1ohpaqirbsjtcukvdwlxeymzfAnBg

65 

66 

67def _step_status(status: CustomDomainStatus, phase: Phase) -> StepStatus: 1CDEFGHI

68 current_phase = PHASE_ORDER[_phase_from_status(status)] 1ohpaqirbsjtcukvdwlxeymzfAnBg

69 step_phase = PHASE_ORDER[phase] 1ohpaqirbsjtcukvdwlxeymzfAnBg

70 if current_phase > step_phase: 1ohpaqirbsjtcukvdwlxeymzfAnBg

71 return "verified" 1haibjckdlemfng

72 if current_phase < step_phase: 1ohpaqirbsjtcukvdwlxeymzfAnBg

73 return "locked" 1ohpaqirbsjtcukvdwlxeymzfAnBg

74 if status == CustomDomainStatus.origin_setup_success: 1ohpaqirbsjtcukvdwlxeymzfAnBg

75 return "verified" 1abcdefg

76 if status in ATTENTION_STATUSES: 1ohpaqirbsjtcukvdwlxeymzfAnBg

77 return "attention" 1abcdefg

78 if status in FAILED_STATUSES: 1ohpaqirbsjtcukvdwlxeymzfAnBg

79 return "failed" 1abcdefg

80 return "in_progress" 1ohpaqirbsjtcukvdwlxeymzfAnBg

81 

82 

83def _describe_step( 1CDEFGHI

84 step_id: StepID, 

85 status: StepStatus, 

86 *, 

87 is_apex: bool = False, 

88) -> str: 

89 if step_id == "ownership": 2Q R o h S T = ? p U a J V W q i X Y @ [ r Z b K 0 1 s j 2 3 ] ^ t 4 c L 5 6 u k 7 8 _ ` v 9 d M ! # w l $ % { | x ' e N ( ) y m * + } ~ z , f O - . A n / : abbbB ; g P

90 if status == "attention": 1ohpaqirbsjtcukvdwlxeymzfAnBg

91 return ( 1abcdefg

92 "We found this record, but its value doesn't match. Update it to " 

93 "the value below. We re-check every minute, no restart needed." 

94 ) 

95 if status == "failed": 1ohpaqirbsjtcukvdwlxeymzfAnBg

96 return ( 1abcdefg

97 "We couldn't confirm ownership. Re-check the value below, then " 

98 "restart verification." 

99 ) 

100 if status == "verified": 1ohpaqirbsjtcukvdwlxeymzfAnBg

101 return "Ownership confirmed." 1haibjckdlemfng

102 return ( 1ohpaqirbsjtcukvdwlxeymzfAnBg

103 "Add this TXT record. We check every minute and unlock the next step " 

104 "automatically." 

105 ) 

106 

107 if step_id == "certificate": 2Q R o h S T = ? p U a J V W q i X Y @ [ r Z b K 0 1 s j 2 3 ] ^ t 4 c L 5 6 u k 7 8 _ ` v 9 d M ! # w l $ % { | x ' e N ( ) y m * + } ~ z , f O - . A n / : abbbB ; g P

108 if status == "locked": 1ohpaqirbsjtcukvdwlxeymzfAnBg

109 return "Unlocks once ownership is verified." 1ohpaqirbsjtcukvdwlxeymzfAnBg

110 if status == "failed": 1haibjckdlemfng

111 return ( 1abcdefg

112 "We couldn't secure your domain. Re-check the records below, then " 

113 "restart verification." 

114 ) 

115 if status == "verified": 1haibjckdlemfng

116 return "Domain verified and TLS certificate issued." 1haibjckdlemfng

117 return ( 1haibjckdlemfng

118 "Add both records so we can issue your TLS certificate. Your live site " 

119 "doesn't change yet." 

120 ) 

121 

122 if step_id == "traffic": 2Q R o h S T = ? p U a J V W q i X Y @ [ r Z b K 0 1 s j 2 3 ] ^ t 4 c L 5 6 u k 7 8 _ ` v 9 d M ! # w l $ % { | x ' e N ( ) y m * + } ~ z , f O - . A n / : abbbB ; g P

123 if status == "locked": 1ohpaqirbsjtcukvdwlxeymzfAnBg

124 return ( 1ohpaqirbsjtcukvdwlxeymzfAnBg

125 "Unlocks once your domain is secured. Your live site won't change " 

126 "until you add the records here." 

127 ) 

128 if status == "attention": 1haibjckdlemfng

129 return ( 1abcdefg

130 "We found these records, but they don't match. Update them to the " 

131 "values below. We re-check automatically." 

132 ) 

133 if status == "failed": 1haibjckdlemfng

134 return ( 1abcdefg

135 "We couldn't confirm your traffic records. Re-check them below, then " 

136 "restart verification." 

137 ) 

138 if status == "verified": 1haibjckdlemfng

139 return "Your domain is live on FastAPI Cloud." 1abcdefg

140 return "Add these records to move traffic to FastAPI Cloud with no downtime." 1haibjckdlemfng

141 

142 if status == "attention": 2Q R S T = ? U J V W X Y @ [ Z K 0 1 2 3 ] ^ 4 L 5 6 7 8 _ ` 9 M ! # $ % { | ' N ( ) * + } ~ , O - . / : abbb; P

143 return ( 2= J @ K ] L _ M { N } O abP

144 "We found your records, but some values don't match. Update them to the " 

145 "values below. We re-check automatically." 

146 ) 

147 if status == "failed": 2Q R S T ? U J V W X Y [ Z K 0 1 2 3 ^ 4 L 5 6 7 8 ` 9 M ! # $ % | ' N ( ) * + ~ , O - . / : bb; P

148 return ( 2? J [ K ^ L ` M | N ~ O bbP

149 "We couldn't complete setup. Re-check the records below, then restart " 

150 "verification." 

151 ) 

152 if status == "verified": 1QRSTUJVWXYZK01234L56789M!#$%'N()*+,O-./:;P

153 return "Your domain is live on FastAPI Cloud." 1JKLMNOP

154 if is_apex: 1QRSTUJVWXYZK01234L56789M!#$%'N()*+,O-./:;P

155 return ( 1JKLMNOP

156 "Add the records below. We'll verify ownership, issue your TLS " 

157 "certificate, and route traffic automatically." 

158 ) 

159 return ( 1QRSTUJVWXYZK01234L56789M!#$%'N()*+,O-./:;P

160 "Add the record below. We'll verify ownership, issue your TLS certificate, " 

161 "and route traffic automatically." 

162 ) 

163 

164 

165def get_setup_steps(domain: CustomDomain) -> list[SetupStep]: 1CDEFGHI

166 is_apex = any(record.type == "A" for record in domain.dns_records) 2Q R o h S T = ? p U a J V W q i X Y @ [ r Z b K 0 1 s j 2 3 ] ^ t 4 c L 5 6 u k 7 8 _ ` v 9 d M ! # w l $ % { | x ' e N ( ) y m * + } ~ z , f O - . A n / : abbbB ; g P

167 

168 if not domain.is_using_pre_validation: 2Q R o h S T = ? p U a J V W q i X Y @ [ r Z b K 0 1 s j 2 3 ] ^ t 4 c L 5 6 u k 7 8 _ ` v 9 d M ! # w l $ % { | x ' e N ( ) y m * + } ~ z , f O - . A n / : abbbB ; g P

169 if domain.setup_successful: 2Q R S T = ? U J V W X Y @ [ Z K 0 1 2 3 ] ^ 4 L 5 6 7 8 _ ` 9 M ! # $ % { | ' N ( ) * + } ~ , O - . / : abbb; P

170 status: StepStatus = "verified" 1JKLMNOP

171 elif domain.setup_failed: 2Q R S T = ? U J V W X Y @ [ Z K 0 1 2 3 ] ^ 4 L 5 6 7 8 _ ` 9 M ! # $ % { | ' N ( ) * + } ~ , O - . / : abbb; P

172 status = "failed" 2? J [ K ^ L ` M | N ~ O bbP

173 elif domain.status in ATTENTION_STATUSES: 2Q R S T = U J V W X Y @ Z K 0 1 2 3 ] 4 L 5 6 7 8 _ 9 M ! # $ % { ' N ( ) * + } , O - . / : ab; P

174 status = "attention" 2= J @ K ] L _ M { N } O abP

175 else: 

176 status = "in_progress" 1QRSTUJVWXYZK01234L56789M!#$%'N()*+,O-./:;P

177 

178 return [ 2Q R S T = ? U J V W X Y @ [ Z K 0 1 2 3 ] ^ 4 L 5 6 7 8 _ ` 9 M ! # $ % { | ' N ( ) * + } ~ , O - . / : abbb; P

179 SetupStep( 

180 id="combined", 

181 title="Verify ownership and route traffic", 

182 description=_describe_step("combined", status, is_apex=is_apex), 

183 status=status, 

184 records=domain.dns_records, 

185 ) 

186 ] 

187 

188 def records_for(group: RecordGroup) -> list[CustomDomainRecord]: 1ohpaqirbsjtcukvdwlxeymzfAnBg

189 return [ 1ohpaqirbsjtcukvdwlxeymzfAnBg

190 record for record in domain.dns_records if _record_group(record) == group 

191 ] 

192 

193 def build_step( 1ohpaqirbsjtcukvdwlxeymzfAnBg

194 step_id: PhasedStepID, 

195 title: str, 

196 phase: Phase, 

197 ) -> SetupStep: 

198 status = _step_status(domain.status, phase) 1ohpaqirbsjtcukvdwlxeymzfAnBg

199 return SetupStep( 1ohpaqirbsjtcukvdwlxeymzfAnBg

200 id=step_id, 

201 title=title, 

202 description=_describe_step(step_id, status), 

203 status=status, 

204 records=records_for(step_id), 

205 ) 

206 

207 return [ 1ohpaqirbsjtcukvdwlxeymzfAnBg

208 build_step("ownership", "Prove ownership", "internal"), 

209 build_step("certificate", "Secure your domain", "external"), 

210 build_step("traffic", "Switch traffic", "origin"), 

211 ]